一.选择排序
1.原理
选择排序比较简单,就是每一次遍历后,将无序区的一个最大(最小)值与无序区的第一个值或最后一个值交换,这样,无序区渐渐的变为有序,最终完成排序。它的时间复杂度也是 O ( n 2 ) O(n^2) O(n2)
2.过程
3.代码
int selectsort(int arry[], int len)
{
int min, i, j, temp;
for(i = 0; i < len - 1; ++i)
{
min = i;
for (j = i; j < len; ++j)
{
if (arry[j] < arry[min])
{
min = j;
}
}
temp = arry[i];
arry[i] = arry[min];
arry[min] = temp;
}
}
二.堆排序
1.堆的概念
假设序列
k
1
,
k
2
,
⋅
⋅
⋅
,
k
n
{k_1,k_2,\cdot\cdot\cdot,k_n}
k1,k2,⋅⋅⋅,kn有且仅当满足以下关系的时候,我们叫它堆:
{
k
i
≤
k
2
i
,
k
i
≤
k
2
i
+
1
或
{
k
i
≥
k
2
i
,
k
i
≥
k
2
i
+
1
其
中
(
i
=
1
,
2
,
⋅
⋅
⋅
,
n
2
)
\begin{cases} k_i ≤ k_{2i}, \\ k_i ≤ k_{2i+1} \end{cases} 或 \begin{cases} k_i ≥ k_{2i}, \\ k_i ≥ k_{2i+1} \end{cases} 其中(i = 1,2,\cdot\cdot\cdot,\frac{n}{2})
{ki≤k2i,ki≤k2i+1或{ki≥k2i,ki≥k2i+1其中(i=1,2,⋅⋅⋅,2n)
注意,顺序序列编号是从1开始的。
前者是小于等于关系,所以我们又称它为:小根堆
后者是大于等于关系,所以我们又称它为:大根堆
2.堆与完全二叉树的关系
那么如果我们通过这个序列(数组)来模拟完全二叉树的话,在树状结构下,堆有以下性质:
每个父节点均大于(小于)它的儿子结点,看图:
假设有这个小根堆:
把它"转换"成完全二叉树的话为:
不难看出,结点n的孩子结点为2n和2n+1
3.堆排序的过程
我们现在知道了,这个堆二叉树(顺序序列模拟的)的根结点一定是最大(小)
的,那么该元素一定是有序的状态。那么我们排序的思路应该为:
- 构造堆
- 除外根结点元素(将堆顶元素除外或者与最后一个元素交换后并除外)
- 重新调整结构,使剩余元素又构成一个堆
- 重复2、3直到所有元素有序
当然,在第2点中,为了使得基本树形结构不被受到破坏,所以我们选择将堆顶元素与末尾元素交换
后,并将末尾元素划为有序区
,也就是除外,不参与第3点的重新调整结构。
所以,在此基础上,我们升序一般就采用大根堆
,降序采用小根堆
。
经过复杂的推导,它的时间复杂度为
O
(
n
l
o
g
2
n
)
O(nlog_2n)
O(nlog2n)
4.构造堆
堆的初始化,我们需要以下几个步骤:
对所有双亲结点进行以下调整堆
操作:
- 若孩子结点均小于父结点,则结束。
- 在非1的情况下:父节点与孩子结点的最大值交换,并对交换后的孩子结点进行1、2的操作。
我们知道,假设一棵满叶子的完全二叉树,设它有n个结点,那么叶子结点数量
(
n
+
1
)
2
\frac{(n+1)}{2}
2(n+1),则父结点有
(
n
−
1
)
2
\frac{(n-1)}{2}
2(n−1)个
然后,假设我们初始化堆,我们就从最后一个父结点开始,向前(从右到左,从下到上)开始循环构造堆。
从代码上来看就是:
// 从最后一个父结点开始,将所有结点给调整一次。
for (i = len/2; i > 0; --i)
{
HeapAdjust(heapArry, i, len);
}
5.调整堆
我们调整堆的话,思路以及说了:
- 若孩子结点均小于父结点,则结束。
- 在非1的情况下:父节点与孩子结点的最大值交换,并对交换后的孩子结点进行1、2的操作。
代码如下:
int HeapAdjust(int arry[], int index, int len)
{
int i;
arry[0] = arry[index]; // arry[0] 没被使用,刚好可以拿来当交换时的临时变量用
for (i = 2*index; i <= len; i*=2) // 从该结点的左孩子开始,且每次循环都直接到孩子的孩子,且i肯定不能超过树的大小
{
if (i < len && arry[i] < arry[i+1]) // 这里是判断i当前孩子是左孩子大还是右孩子大,将i指向最大的孩子
{
++i;
}
// 把上者<改成>,下者>=改为<=,则该堆排序变成降序
if (arry[0] >= arry[i]) // 如果我们的处理的结点大于了孩子结点,那么就必须交换。
{
break;
}
else
{
arry[index] = arry[i]; // 我们处理的结点被孩子顶替
index = i; // 我们新处理的结点变成了被顶替的孩子位置
}
}
arry[index] = arry[0]; // 一直到了最后,孩子一直顶替,直到没法顶替了,就是我们最开始待处理结点的位置
}
图例:
假设我们只差根结点未调整了,现在要对index=1的结点进行调整:
首先进来,确定我们的元素状态:5是待调整,7是当前指向儿子:
此时,[0]<[i],也就是5<7,因该发生调整,所以[1]因该等于[2],然后待调整的从[1]变到了[2],指向的儿子又变到了[4]:
此时,[0]<[i],也就是5<6,因该发生调整,所以[2]因该等于[4],然后待调整的从[2]变到了[4],指向的儿子又变到了[8](不存在):
那么退出循环后,我们就确定好了待调整位置index=4,所以讲temp也就是arry[0]的值给arry[4]:
这样就完成了一次堆调整。
6.代码
具体的过程可以自己中途输出HeapSort来看堆排序与构造的过程。
#include <stdio.h>
#include <stdlib.h>
int HeapAdjust(int arry[], int index, int len)
{
int i;
arry[0] = arry[index]; // arry[0] 没被使用,刚好可以拿来当交换时的临时变量用
for (i = 2*index; i <= len; i*=2) // 从该结点的左孩子开始,且每次循环都直接到孩子的孩子,且i肯定不能超过树的大小
{
if (i < len && arry[i] < arry[i+1]) // 这里是判断i当前孩子是左孩子大还是右孩子大,将i指向最大的孩子
{
++i;
}
// 把上者<改成>,下者>=改为<=,则该堆排序变成降序
if (arry[0] >= arry[i]) // 如果我们的处理的结点大于了孩子结点,那么就必须交换。
{
break;
}
else
{
arry[index] = arry[i]; // 我们处理的结点被孩子顶替
index = i; // 我们新处理的结点变成了被顶替的孩子位置
}
}
arry[index] = arry[0]; // 一直到了最后,孩子一直顶替,直到没法顶替了,就是我们最开始待处理结点的位置
}
int HeapSort(int arry[], int len)
{
int i = 1;
int *heapArry = (int*)malloc(sizeof(int) * (len+1)); // 构造一个从下标1开始的序列。
for (i = 1; i <= len; ++i)
{
heapArry[i] = arry[i - 1];
}
// 从最后一个父结点开始,将所有结点给调整一次。
for (i = len/2; i > 0; --i)
{
HeapAdjust(heapArry, i, len);
}
// 堆排序。
for (i = len; i > 0; --i)
{
arry[i-1] = heapArry[1]; // 堆顶是我们的最大元素,赋值给原数组
heapArry[1] = heapArry[i]; // 因为是交换,所以要把最后一个元素给堆顶,堆顶给最后一个元素(有序),但我们可以舍弃这个保存,因为存到了老数组里
HeapAdjust(heapArry, 1, i - 1); // i之后的结点是有序的(尽管没有赋值),所以不参与堆的构造
}
}
int main()
{
int a[7] = {5,6,3,7,2,1,4};
int i;
HeapSort(a, 7);
for (i = 0; i < 7; ++i)
{
printf("%d ", a[i]);
}
return 0;
}