文章目录
- 1. 简单选择排序
- 简单选择排序算法
- 简单排序算法分析
- 2. 堆排序
- 堆的定义
- 堆的调整
- 堆的建立
- 堆排序算法
- 堆排序算法分析
1. 简单选择排序
基本思想
- 在待排序的数据中选出最大(小)的元素放在其最终的位置。
基本操作
- 首先通过 n - 1 次关键字比较,从 n 个记录中找出关键字最小的记录,将它与第一个记录交换。
- 在通过 n - 2 次比较,从剩余的 n - 1个记录汇总找出关键字次小的记录,将它与第二个记录交换。
- 重复上述操作,总共进行 n - 1 趟排序后,排序结束。
举个栗子
- 把最小的数据元素挑出来,放到第一个位置。
- 找出第二小的数据元素,放到第二个位置上。
- 接下来找出第三小的元素,放到第三个位置上。
- 之后就依次类推,跑完 6-1=5 趟之后,得到排序完成为止。
结论
- 虽然每一趟都要找一个最小值,但是找最小值的范围是不同的。
- 第一趟从第二个元素开始,直到最后一个元素找最小值和第一个位置进行交换,第二趟从第二个开始到最后找出最小值和第二个交换……
- 第 i 趟从第 i 个元素开始,到最后一个元素找最出小值和第 i 个进行交换。
找最小值
- 假设第 1 个元素 A 为当前最小值,然后依次将 A 与之后面的元素进行比较。
- 如果后面出现了比 A 小的值 B,则 B 的位置记录下来,将最小值更新为 B。
- 然后用新的最小值 B 和后面的元素进行比较;
- 出现比 B 小的值 C 就将最小值更新成 C 继续执行上述步骤,
- 如果直到最后都没直到比 B 小的,则 B 为最小值,将其放到第 i 个位置。
简单选择排序算法
//对顺心表L做简单做简单选择排序
void SelectSort(SqList &K)
{
for(i = 1;i < L.length;i++)//在L.e[i...L.length] 中选择关键字最小的记录
{
k = i;//将当前最小值的位置用K记录下来
for(j = i + 1;j <= L.length;j++)
{
if(L.r[j].key < L.r[k].key) //某个元素比下标为k的还小,则它就是当前的最小值
{
k = j;//k指向此趟排序中关键字最小的记录
}
}
if(k != i) //如果第一个不是最小值了,则交换 r[i] 与 r[k]
{
t = L.r[i];
L.r[i] = L.r[k];
L.r[k] = t;
}
}
}
简单排序算法分析
时间复杂度
- 记录移动次数:
- 最好情况:0。
- 最坏情况:3(n-1)。
- 比较次数:无论待排序列处于什么状态,选择排序所需进行的比较次数都相同。
- 时间复杂度:O(n2)
算法稳定性
- 简单选择排序是不稳定排序。
空间复杂度
- 在进行交换的时候,需要一个额外的辅助空间,所以时间复杂度为:O(1)。
2. 堆排序
堆的定义
- 小根堆:第 i 个位置上的元素小于第 2i 及第 2i+1 位置上的元素;
- 如:第 2 个位置上的元素必须小于第 4 及第 5 个元素。
- 大根堆:与小根堆正好相反。
- 从堆的定义可以看出,堆实际上是满足如下性质的完全二叉树:
- 二叉树中任一非叶子结点均小于(大于)等于它的左右孩子结点。
- 根结点的值小于等于左右孩子的称作小根堆,大根堆反之。
举个例子
【例1】判断以下两组数据是否是堆:
- {98,77,35,53,55,14,35,45}
- {14,48,35,62,55,98,35,77}
- 按照自上而下、自左而右的方式,依次所有的数据数据,看看是否符合大、小根堆的定义。
【例2】判断是否是堆?
堆排序
- 若在输出堆顶的最小值(最大值)后,使得剩余 n-1 个元素的序列重新又建立成一个堆,则得到 n 个元素的次小值(次大值)……如此反复,便能得到一个有序序列,这个过程称之为堆排序。
- 在大根堆中,根结点就是树的最大值,小根堆则是最小值。
- 将堆顶拿走之后,将剩下的元素重新调整成一个堆,那么新堆的根节点就是当前的最大(最小)值了。
实现堆排序需要解决两个问题
- 堆建立:如何由一个无序序列建成一个堆?
- 堆调整:如何在输出堆顶元素后,调整剩余的元素为一个新堆?
堆的调整
如何在输出堆顶元素之后,调整剩余元素为一个新的堆?
小根堆
- 输出堆顶元素之后,以堆中最后一个元素 A 代替它成为根节点;
- 然后将根节点 A 的值与左、右子树的根节点值进行比较,并与其中小者进行交换;
- 重复上述操作,直到将 A 调整回叶子结点,就能得到新的堆,将这个从堆顶至叶子的调整过程称为筛选。
大根堆的调整方式与小根堆相同,只不过变成了让根结点与比它大的左右孩子进行交换而已。
举个例子
当前有一棵小根堆构成的二叉树。
- 输出根节点并用最后一个元素代替它,接下来就一只进行交换调整,直到将这个结点调整回叶子结点去。
- 将它的左右孩子的值与它进行比较,并和其中的较小值进行交换。
- 将 97 拿下来之后它并没有成为叶子,所以继续将其与左右孩子比较然后交换。
- 当此结点成为叶子结点之后,小根堆调整完毕。
- 之后再输出堆顶元素然后调整的过程相同。
筛选过程算法
/*已知R[s...m]中记录的关键字除了R[s]之外均满足堆的定义,
将R[s...m]调整为以R[s]为根的大根堆*/
void HeapAdjust(elem R[],int s,int m)
{
rc = R[s];
for(j = 2*s;j <= m;j *= 2)//沿着key较大的孩子结点向下筛选
{
if(j < m&& R[j] < R[j+1])
{
++j;//j为key较大的孩子结点的下标
}
if(rc >= R[j])
{
break;
}
R[s] = R[j];
s = j;//rc应该插入在位置s上
}
R[s] = rc;//插入
}
- s 是根节点下标,m是数组最大下标,j 被赋值为 s 的左孩子下标;
- 第一个 if 对两个子节点进行比较,选最大的子节点,在第二个 if 中与根节点的值比较,如果根节点的值更大则直接退出,保持不变,否则将大的子节点的值赋值给根节点的位置;
- 然后将子节点的下标赋值给 s,进行循环继续比较,循环结束后将之前存的根节点的值赋值在叶子节点上。
结论
- 对一个无序序列反复删选就可以得到一个堆;
- 即:从一个无序序列建堆的过程就是一个反复筛选的过程。
堆的建立
如何由一个无序序列建成一个堆?
显然
- 只有一个结点的二叉树是堆;
- 在完全二叉树中,所有序号大于 n /2 的结点都是叶子结点,因此以这些结点为根的子树均已是堆。
- 所有的叶子结点都不需要调整,它已经是堆了。
- 只需要调整所有的非叶子结点。
- 根据完全二叉树的性质,最后一个叶子如果为 n ,那么它的双亲结点为 n / 2,也就数说,最后一个非叶子结点是 n / 2。
- 只需要利用筛选法,从最后一个非叶子结点 n / 2 开始,依次将序号为 n / 2、(n / 2) - 1…、1 的结点作为根的子树都调整为根即可。
由于堆实质是一个线性表,那么我们就可以顺序存储一个堆。
例:建造一个小根堆
有关键字为:49,38,65,97,76,13,27,49 的一组记录,将其按照关键字调整为一个小根堆。
- 先把这些元素按照序号自上而下、自左而右的顺序建立一棵初始完全二叉树。
- 从最后一个非叶子结点开始,开始往前依次调整:
- 调整从第 n / 2 个元素开始,将以该节点为根的二叉树调整为小根堆。
- 将 97 和 49 两个结点交换一下,对应的数组位置也要交换。
- 将以序号为 n/2 - 1的结点作为根的二叉树调整为堆。
- 现在 3 号位置的结点调整完了,接下来该调整 2 号结点了。
- 现在该调整 2 号结点了,将以序号为 n/2 - 2 的结点为根的二叉树调整为堆;
- 发现 38 的左右孩子都比它大,不需要要调整。
- 再将以序号为 n/2 - 3 的结点为根的二叉树调整为堆。
- 将根节点 49 与 13 交换位置,此时还没有调整结束,还需要将根结点 49 调整到叶子结点才行。
- 最后将 49 与 27 交换位置,至此,整个无序树就变成了一个小根堆了。
算法描述
将初始无序的R[1]到R[n]建成一个小根堆,可用一下语句实现:
for(i = n/2;i >= 1;i--)
{
HeapAdjust(R,i,n);
}
堆排序算法
由以上分析知:
- 若对一个无序序列建堆,然后输出根;重复该过程就可以由一个无序序列输出为有序序列。
- 实际上,堆排序就是利用完全二叉树中父结点与孩子结点之间的内在关系来排序的(实际就是二叉树的性质5)。
堆排序算法
//对R[1]到R[n]进行堆排序
void HeapSort(elem R[])
{
int i;
for(i = n/2;i >= 1;i--)//从最后一个非叶子结点到第一个来建立初始堆
{
HeapAdjust(R,i,n);//建立初始堆
}
for(i = n;i > 1;i--)//进行n-1趟排序
{
Swap(R[1],R[i]);//将根与最后一个元素交换
HeapAdjust(R,j,i-1);//对R[1]到R[i-1]重新建堆
}
}
堆排序算法分析
时间复杂度
- 初始堆化所需时间不超过 O(n)。
- 排序阶段(不含初始堆化)
- 一次重新堆化所需时间不超过 O(logn)。
- n-1 次循环所需时间不超过 O(nlogn)。
- Tw(n) = O(n) + O(nlogn) = O(nlogn)
- 堆排序的时间主要耗费在建立初始堆和调整建新堆时进行的修复筛选上。
- 堆排序在最坏情况下,其时间复杂度也为 O(nlog₂n),这是堆排序的最大有点。
- 无论待排序中的记录时正序还是逆序排列,都不会使堆排序处于“最好”或“最坏”的状态。
空间复杂度
- 仅需要一个大小供交换用的辅助空间,所以空间复杂度为 O(1)。
堆排序算法特点
- 是不稳定排序。
- 只能用于顺序结构,不能用于链式结构。
- 初始建堆所需的比较次数较多,因此记录数较少时不宜采用。
- 堆排序在最坏情况下时间复杂度为 O(nlog₂n),相对于快速排序最坏情况下的 O(n2) 而言是一个优点,当记录较多时比较高效。