所谓排序,就是要将一堆记录,使之按关键字递增(或递减)次序排列起来。根据排序所采用的策略,可以分为如下五种:
3、选择排序(直接选择排序、堆排序)
4、归并排序;
5、桶排序(桶排序,基数排序);
---------------------------------------------------------------------------------
选择排序(Selection Sort)的基本思想是:每一趟从待排序的记录中选出关键字最小的记录,顺序放在已排好序的子文件的最后,直到全部记录排序完毕。
常用的选择排序方法有直接选择排序和堆排序。
直接选择排序
基本思想:将待排序记录分成有序区和无序区,初始状态下有序区位空,无序区为整个待排序记录。每一趟选择排序就是在无序区中选择最小(或最大)的记录,插入到有序区的最后。如此循环直到无序区为空,完成排序。
代码实现// 直接选择排序
//
void select_sort(int* array, int length)
{
assert(array && length >= 0);
int i, j, k, temp;
for (i = 0; i < length-1; ++i) {
k = i;//用来记录哪个位置的值小
for (j = i + 1; j < length; ++j) {
if (array[j] < array[k]) {
k = j;//如果有比原选拔最小值小的元素,则k更新
}
}
if (k != i) {//内循环结束时,将选拔出那个最小值放到对应位置上,即与i位置上的值交换
temp = array[i];
array[i] = array[k];
array[k] = temp;
}
}
}
时间复杂度分析:
无论待排序记录的初始状态如何,在第 i 趟排序中选出最小关键字的记录,需做 n - i 次比较,因此,总的比较次数为: n * (n - 1) / 2 = 0(n ^ 2),当待排序记录的初始状态为正序时,移动次数为 0;当初始状态为反序时,每趟排序均要执行交换操作,总的移动次数取最大值 3 * (n-1)。所以,直接选择排序的平均时间复杂度为 0(n ^ 2)。
空间复杂度:
很明显,0(1)。
补充:
直接选择排序是一个就地排序,且是非稳定排序。
堆排序
堆的定义:满足如下约束的 n 个关键字序列 Kl,K2,…,Kn 称为堆,1 ≤ i ≤ n/2,
(1) ki ≤ K2i 且 ki ≤ K2i+1 (小根堆) 或
(2) Ki ≥ K2i 且 ki ≥ K2i+1 (大根堆)
从定义来看,堆实质上是满足如下性质的完全二叉树:树中任一非叶结点的关键字均不大于(或不小于)其左右孩子(若存在的话)结点的关键字。堆排序就是充分利用堆这种堆顶记录要么是最大的要么是最小的有序性质,每次在堆中选择最大或最小关键字完成排序。堆排序也是选择排序的一种,它比直接选择排序平均效率要高,因为直接选择排序,每次比较之后并没有对比较结果进行保存,导致可能存在重复的比较,而堆则利用其堆性质将比较结果保存在堆中了(非叶子节点的左边孩子一定不大于或不小于比右边的孩子)。
堆排序的关键就在于将待排序记录的建立成堆,建好堆之后,将无序区的堆顶记录(总是无序区的最大或最小)和无序区最后一个记录交换,交换之后,无序区的最后一个记录变成有序区的第一个记录,而无序区新的的堆顶记录不一定满足堆性质了,因而需要将无序区调整而堆。如此循环,直到无序区为空,结束排序。
ps:有一个点被删时需要调整,因为相当于数组少了一个元素,所以无论如何最后一个元素都需要移动到前面某个位置,调整就是要给最后这个元素找到合适的位置并将它放在那里。创建一个堆时需要依次插入元素,每次插入相当于在数组最后添加了一个元素,这个元素需要经过调整去找到它的合适位置
所以堆排序的主要步骤就分三步:
1,建立初始堆;
2,将无序的堆顶记录与最后一个记录交换,缩小无序区;
3,将交换之后的无序区调整为堆,重复步骤 2。
代码实现:堆排序后取出的最值点都会依次放到数组最后以保证就地排序,这样如果想保证排序后数组为从小到大,则需要每次取出的为最大值,即大堆排序
// 筛选法调整堆,除 [low] 之外,[low] 的两个孩子均已是大根堆//第二个参数表示以它为对顶的子堆以满足性质,所以要以他开始向上找合适的位置,第三个参数相当于子堆得最后一个点,所谓的调整也就是要把这个点移动到合适的位置
void adjust_heap(int* heap, int low, int last){
assert(heap);
// 循环实现
int i = low;
int child = 2 * i;
int temp = heap[i];
while (child<= high) {
// 若有两个孩子,j 为孩子中大的那个的下标
if (child < high && heap[child] < heap[child + 1]) {
j = j + 1;
}
// 已是堆
if (temp >= heap[j]) {
break;
}
// 继续筛选
else {
heap[i] = heap[j];
i = j;
j = 2 * i;
}
}
heap[i] = temp;
}
// 只有一个结点的树是堆,而在完全二叉树中,所有序号 i > n/2 的结点都是叶子,
// 因此以这些结点为根的子树均已是堆。这样,我们只需依次将以序号为
// n/2, n/2 - 1, …, 0 的结点作为根的子树都调整为堆即可。
void build_heap(int* heap, int length)
{
assert(heap && length >= 0);
int i;
for(i = length / 2; i >= 0; --i) {//length/2表示倒数第二层的最后一个节点(因为是从0开始的)
adjust_heap(heap, i, length - 1);
}
}
// 堆排序
//
void heap_sort(int* array, int length)
{
assert(array && length >= 0);
if (length <= 1) {
return;
}
int i, temp;
// 将 [0, length - 1] 建成初始堆
build_heap(array, length);
// 对当前无序区 [0, i - 1] 进行堆排序,共做 length - 1 趟。
for(i = length - 1; i > 0; --i) {
// 将堆顶和堆中最后一个记录交换
temp = array[0];
array[0] = array[i];
array[i]= temp;
// 将 [0, i - 1] 重新调整为堆,仅有 [0] 可能违反堆性质
adjust_heap(array, 0, i - 1);//i-1为堆中最后一个元素,0说明0以下的那些节点都已满足堆性质
}
}
时间复杂度分析:
堆排序的时间开销主要由建立初始堆和反复调整堆这两部分的时间开销构成,你可以想象成二叉树的情形。堆排序的平均时间复杂度为O(nlgn)。
空间复杂度分析:
很明显,为 0(1)。
补充:
堆排序是不稳定的就地排序。
转载自(http://www.cppblog.com/kesalin)