排序就是按照某种比较方式对一堆数据进行处理,处理之后的数据变得有序。
- 本文所讲的排序都是升序的。
直接插入排序 — InsertSort
基本思想:将一个数插入到有序序列中,保持序列的有序,得到一个新的有序序列
void InsertSort(int* a, int n)
{
for (int i = 0; i < n - 1; i++)
{
int end = i;
int tmp = a[end + 1];//要排序的数据
while (end >= 0)
{
if (a[end] > tmp)
{
a[end + 1] = a[end];
end--;
}
else
break;
}
a[end + 1] = tmp;
//走到这里,有两种情况:
//1.break跳出
//2.while循环结束
}
}
时间复杂度 O( N 2 N^2 N2)
最好情况:序列顺序有序或接近有序,此时时间复杂度为O(N)
最坏情况:序列逆序或接近逆序,每次插入时,每个位置数据都要往后挪,此时时间复杂度为O( N 2 N^2 N2)
空间复杂度O(1)
希尔排序 — ShellSort
上面我们说到,直接插入排序在序列逆序或接近逆序时,非常坏。一个叫希尔的人对直接插入排序进行了优化。
预排序:让大的数快速到后面,小的数快速到前面,从而让数组接近有序。
基本思想:先进行预排序,最后进行一次直接插入排序。选一个gap,把序列分成gap个组,每个组进行预排序,然后gap减小,重复上述的分组和排序,当gap == 1 时,就是直接插入排序。
void ShellSort(int* a, int n)
{
int gap = n;
while (gap > 1)
{
gap = gap / 3 + 1;
//在取gap值时,可以和我写的一样,也可以用下面这种写法
// gap = gap/2;
//但要保证除到最后gap == 1
//gap/3+1这种写法可以让让gap循环次数减少一些
for (int i = 0; i < gap; i++) //把序列分为gap组
{
for (int j = i; j < n - gap; j += gap)//对每一组经行预排序
{
int end = j;
int tmp = a[end + gap];
while (end >= 0)
{
if (a[end] > tmp)
{
a[end + gap] = a[end];
end -= gap;
}
else
break;
}
a[end + gap] = tmp;
}
}
}
}
有人对上面的写法进行了优化
void ShellSort(int* a, int n)
{
int gap = n;
while (gap > 1)
{
gap = gap / 3 + 1;
for (int j = 0; j < n - gap; j++)
//这种写法和上面的写法一样,上面的写法是依次对每一组进行预排序,这里是每一组同时来进行预排序
{
int end = j;
int tmp = a[end + gap];
while (end >= 0)
{
if (a[end] > tmp)
{
a[end + gap] = a[end];
end -= gap;
}
else
break;
}
a[end + gap] = tmp;
}
}
}
算一下时间复杂度:
1.刚开始时,gap较大,此时的时间复杂度为O(N)
2.当gap较小时,原序列因为预排序已经接近有序了,所以时间复杂度也为O(N)
时间复杂度 O( N 1.3 N^{1.3} N1.3)
要推导比较麻烦,直接记结论
空间复杂度O(1)
直接选择排序 — SelectSort
基本思想:遍历原序列,每次找出一个最大值或最小值,这里写代码时可以优化一下,每次找一对最大值和最小值
void Swap(int* a, int* b)
{
int tmp = *a;
*a = *b;
*b = tmp;
}
void SelectSort(int* a, int n)
{
int begin = 0;
int end = n - 1;
while (begin < end)//begin == end,就剩最后一个数据,他就在他正确的位置上
{
int min = begin;
int max = begin;
for (int i = begin; i <= end; i++)
{
if (a[i] < a[min])
min = i;
if (a[i] > a[max])
max = i;
}
Swap(&a[begin], &a[min]);
if (begin == max) // 如果最大值在begin位置上,max = min;
max = min;
Swap(&a[end], &a[max]);
begin++;
end--;
}
}
时间复杂度 O( N 2 N^2 N2)
直接选择排序是一个非常差的排序,即使数据有序,它也要不断遍历序列,也就是说不论最好还是最坏,他的时间复杂度都是O(N^2)
空间复杂度O(1)
堆排序 — HeapSort
基本思想:利用堆的特性,进行排序
void AdjustDown(int* a, int n, int parent)
{
int child = parent * 2 + 1;
while (child < n)
{
if (child + 1 < n && a[child] < a[child + 1])
child++;
if (a[parent] < a[child])
{
Swap(&a[parent], &a[child]);
parent = child;
child = 2 * parent + 1;
}
else
break;
}
}
void HeapSort(int* a, int n)
{
for (int i = (n - 1 - 1) / 2; i >= 0; i--) // 建堆 O(logN)
{
AdjustDown(a, n, i);
}
int end = n - 1;
while (end > 0)
{
Swap(&a[0], &a[end]);
AdjustDown(a, end, 0);
end--;
}
}
时间复杂度O(N* l o g 2 N log^N_2 log2N)
空间复杂度O(1)
冒泡排序 — BubbleSort
基本思想:遍历n-1次,每次把最大的值交换到最后
void BubbleSort(int* a, int n)
{
for (int i = 0; i < n - 1; i++)
{
int flag = 1;
for (int j = 0; j < n - 1 - i; j++)
{
if (a[j] > a[j + 1])
{
Swap(&a[j], &a[j + 1]);
flag = 0;
}
}
if (flag)
break;
}
}
时间复杂度O( N 2 N^2 N2)
优化之后,如果顺序有序,时间复杂度为O(N)
空间复杂度O(1)
快速排序 — QuickSort
基本思路:把一个数排到它在原序列的最终位置上,那这个数就需要参与之后的排序了,重复递归这个过程,最终整个序列就排好序了。
hoare版本(左右指针法)
基本思路:右边找小,左边找大,交换,重复这个过程。相遇时交换keyi和left/right
//hoare
void QuickSort1(int* a, int left, int right)
{
//如果区间不存在或者只有一个数的时候,就可以返回了
if (left >= right)
return;
int keyi = left;
int beign = left;
int end = right;
while (beign < end)
{
while (beign < end && a[end] >= a[keyi])//右边找小
end--;
while (beign < end && a[beign] <= a[keyi])//左边找大
beign++;
Swap(&a[beign], &a[end]);
}
Swap(&a[keyi], &a[end]);
//[left, end-1] end [end+1, right]
QuickSort1(a, left, end - 1);
QuickSort1(a, end + 1, right);
}
这里是怎么能保证最后相遇时的数(a[begin/end])要小于a[keyi]的呢
只要end先走就可以保证
1.begin不动end动,end往左走找小的,结果没找到,和begin相遇了,此时的begin和end在经过上轮交换之后,a[begin]<a[keyi]
2.end不动begin动,begin往右走找大的,结果没找到,和end相遇了,此时的end已经找到小了,所以a[begin] < a[keyi]
挖坑法
挖坑法是对hoare法的思路上的优化
基本思路:povit是坑,右边找小,a[povit] = a[end], end成为新的坑,右边找大,a[povit] = a[begin],begin成为新的坑,重复这个过程。
void QuickSort2(int* a, int left, int right)
{
if (left > right)
return;
int povit = left;
int tmp = a[povit];
int begin = left;
int end = right;
while (begin < end)
{
while (begin < end && a[end] >= tmp)
end--;
a[povit] = a[end];
povit = end;
while (begin < end && a[begin] <= tmp)
begin++;
a[povit] = a[begin];
povit = begin;
}
a[end] = tmp;
//[left, end-1] end [end+1, right]
QuickSort2(a, left, end - 1);
QuickSort2(a, end + 1, right);
}
前后指针法
基本思路:curr不断遍历序列,把比a[keyi]小的换到左边,大的换到右边
void QuickSort3(int* a, int left, int right)
{
if (left >= right)
return;
int keyi = left;
int prev = left;
int curr = left + 1;
while (curr <= right)
{
if (a[curr] < a[keyi] && ++prev != curr)
Swap(&a[curr], &a[prev]);
curr++;
}
Swap(&a[keyi], &a[prev]);
//[left, prev-1] prev [prev+1, right]
QuickSort3(a, left, prev - 1);
QuickSort3(a, prev + 1, right);
}
优化
三数取中
快排在一些特殊情况下会很坏,比如一个有序序列,从右边找小就会遍历一遍序列,此时快排的时间复杂度为O( N 2 N^2 N2),在加了三数取中之后,就不会有这么坏的情况了。
int GetMid(int* a, int left, int right)
{
int mid = (left + right) / 2;
if (a[mid] > a[left])
{
if (a[right] > a[mid])
return mid;
else // 此时mid是最大数的下标,中间数是left和right的较大值
return a[left] > a[right] ? left : right;
}
else // a[mid] < a[left]
{
if (a[mid] > a[right])
return mid;
else //此时mid是最小数的下标,中间数是left和right的较小值
return a[left] > a[right] ? right : left;
}
}
小区间优化
快速排序的递归过程类似于一颗二叉树,二叉树的倒数一层的节点占总节点的50%,倒数二层的节点占总节点的25%,倒数三层的节点占总节点的12.5%,所以快速排序的最后三层递归占80%多,当递归到小的子区间时,可以使用其他排序,减少栈帧的消耗。
void QuickSort1(int* a, int left, int right)
{
//如果区间不存在或者只有一个数的时候,就可以返回了
if (left >= right)
return;
int index = GetMid(a, left, right);
Swap(&a[index], &a[left]);
int keyi = left;
int beign = left;
int end = right;
while (beign < end)
{
while (beign < end && a[end] >= a[keyi])//右边找小
end--;
while (beign < end && a[beign] <= a[keyi])//左边找大
beign++;
Swap(&a[beign], &a[end]);
}
Swap(&a[keyi], &a[end]);
//[left, end-1] end [end+1, right]
//小区间优化
if (end - 1 - left > 10)
QuickSort1(a, left, end - 1);
else
InsertSort(a + left, end - left);
if (right - end - 1 > 10)
QuickSort1(a, end + 1, right);
else
InsertSort(a + end + 1, right - end);
}
快速排序非递归 — QuickSortNonR
int PartSort(int* a, int left, int right)
{
int index = GetMid(a, left, right);
Swap(&a[index], &a[left]);
int keyi = left;
int prev = left;
int curr = left + 1;
while (curr <= right)
{
if (a[curr] < a[keyi] && ++prev != curr)
Swap(&a[curr], &a[prev]);
curr++;
}
Swap(&a[keyi], &a[prev]);
return prev;
}
void QuickSortNonR(int* a, int n)
{
Stack st;
StackInit(&st);
StackPush(&st, n - 1);
StackPush(&st, 0);
while (!StackEmpty(&st))
{
int left = StackTop(&st);
StackPop(&st);
int right = StackTop(&st);
StackPop(&st);
int index = PartSort(a, left, right);
// [left, index-1] index [index+1, right]
if (right > index+1)
{
StackPush(&st, right);
StackPush(&st, index + 1);
}
if (index - 1 > left)
{
StackPush(&st, index - 1);
StackPush(&st, left);
}
}
StackDestroy(&st);
}
时间复杂度O(N* l o g 2 N log^N_2 log2N)
空间复杂度O( l o g 2 N log^N_2 log2N)
归并排序 — MergeSort
基本思路:如果两个序列时有序,那么将子序列合并,就可以得到完全有序的序列,即每个子序列有序,再使子序列段间有序。
void _MergeSort(int* a, int left, int right, int* tmp)
{
if (left >= right)
return;
int mid = (left + right) / 2;
_MergeSort(a, left, mid, tmp);
_MergeSort(a, mid + 1, right, tmp);
int begin1 = left, end1 = mid;
int begin2 = mid + 1, end2 = right;
int index = left;
while (begin1 <= end1 && begin2 <= end2)
{
if (a[begin1] < a[begin2])
tmp[index++] = a[begin1++];
else
tmp[index++] = a[begin2++];
}
while (begin1 <= end1)
tmp[index++] = a[begin1++];
while (begin2 <= end2)
tmp[index++] = a[begin2++];
memcpy(a + left, tmp + left, sizeof(int) * (right - left + 1));
}
void MergeSort(int* a, int n)
{
int* tmp = (int*)malloc(sizeof(int)*n);
if(!tmp)
{
perror("malloc fail");
exit(-1);
}
_MergeSort(a, 0, n - 1, tmp);
free(tmp);
}
归并排序非递归 — MergeSortNonR
void MergeSortNonR(int* a, int n)
{
int* tmp = (int*)malloc(sizeof(int) * n);
if (!tmp)
{
perror("malloc fail");
exit(-1);
}
int gap = 1;
while (gap < n)
{
for (int i = 0; i < n; i += 2*gap)
{
int begin1 = i, end1 = i + gap - 1;
int begin2 = i + gap, end2 = i + 2 * gap - 1;
int index = i;
// 因为 i < n, 所以begin1永远不会越界
// end1, begin2, end2, 都可能会越界
// 当end1/begin2越界时,右半区间就不存在,左边区间就不用动了
// 当end2越界时,右半区间还有值,就要调整end2的位置
//if(end1 >= n || begin2 >= n)
if (begin2 >= n) //end1越界,那begin2一定越界
break;
if (end2 >= n)
end2 = n - 1;
while (begin1 <= end1 && begin2 <= end2)
{
if (a[begin1] <= a[begin2])
tmp[index++] = a[begin1++];
else
tmp[index++] = a[begin2++];
}
while (begin1 <= end1)
tmp[index++] = a[begin1++];
while (begin2 <= end2)
tmp[index++] = a[begin2++];
memcpy(a + i, tmp + i, sizeof(int) * (end2 - i + 1));
}
gap *= 2;
}
free(tmp);
}
时间复杂度 O(N* l o g 2 N log^N_2 log2N)
空间复杂度 O(N)
稳定性
稳定性是指两个相同的值,在排完序后,相对位置不变。
InsertSort:稳定 — 在比较时,比tmp大才往后挪,相等就放在后面。
ShellSort:不稳定 — 预处理时相同的值可能在不同的组中
SelectSort:不稳定
例如: 3,3,1,…
如果最小数是1,那3的相对位置就改变了
HeapSort:不稳定 — 如果堆中都是相同的数,在交换时,相对位置就变了
BubbleSort:稳定 — 前一个比后一个大,交换, 相等,不交换,相对位置不变
QuickSort:不稳定
例如: 5,3,5,…,5…
如果最左边的5是keyi,那么相对位置就会发生改变
MergeSort:稳定 — 左区间和右区间的值相等时,左区间先入