排序
排序就是将一堆杂乱无章的数据,按照每个元素的大小,以递增或者递减的形式排列数据。
稳定性:如果多个相同的元素,排序完,它们的前后顺序保持不变,则这个算法是稳定的;如果它们的前后顺序不一致,算法是不稳定。
本次博客中算法都是排序升序的。
插入排序
每次将一个新的元素插入到已经有序的序列中,直到全部元素插入完就排序好了。
第一趟插入,从第二个数开始,5比3大,直接插入。
第二趟,4和5比,4比5小,第三个位置的值赋值为5,4比3大,直接插入。
// 插入排序
void InsertSort(int* a, int size)
{
// size个数据排序size-1次
for (int i = 0; i < size - 1; ++i)
{
// 下标0到end有序,从下标为end数据开始比较
int end = i;
// end位置的下个数据
int t = a[end + 1];
// 当比较完下标为0的第一个元素结束
while (end >= 0)
{
// 如果比插入数据小,覆盖
if (t < a[end])
{
a[end + 1] = a[end];
--end;
}
// 比插入数据大,跳出循环插入数据
else
{
break;
}
}
// t是最小的时候,end为-1,循环里不插入数据,统一在循环外面插入数据
a[end + 1] = t;
}
}
时间复杂度:O(N^2)。
空间复杂度:O(1)。
稳定性:稳定。
元素集合越接近有序,使用插入排序算法效率越高。
希尔排序
希尔排序是建立在插入排序的基础上的。多次将元素集合进行分组,间距为gap的为一组,每组的元素先自行排序。
gap越大的时候,大的元素更快到后面,小的元素更快到前面,但是越不接近有序;gap越小的时候,大的元素更慢到后面,小的元素更慢到前面,但是越接近有序。
多次预排序,让元素更快的排在有序位置的附近,最后gap为1进行插入排序,完成排序。
int gap = size;
gap = gap / 3 + 1;
gap是一个变化的值,并且要与元素集合个数有关,+1是为了一定会有gap为1,当gap为1排序完结束。 这里gap是3,相同颜色的为一组。
预排序。
gap为1。
插入排序
// 希尔排序
void ShellSort(int* a, int size)
{
int gap = size;
// gap为1排序完成
while (gap > 1)
{
// 分组
gap = gap / 3 + 1;
// size个数据比较size-gap次
for (int i = 0; i < size - gap; ++i)
{
// 该组有序的最后一个数据
int end = i;
// 同一个组的下个数据
int t = a[end + gap];
// 当end小于0的时候结束
while (end >= 0)
{
// 如果比插入数据小,覆盖
if (t < a[end])
{
a[end + gap] = a[end];
end -= gap;
}
// 比插入数据大,跳出循环
else
{
break;
}
}
// 插入数据
a[end + gap] = t;
}
}
}
不建议直接将gap分组的直接排序好,会有嵌套三层循环,每次每组插入一个。整体思路和插入排序一样,把-1的地方换为-gap,要理解为什么是这样。
时间复杂度:O(N*log3(N))。(N乘以3为底N的对数)。希尔排序的时间复杂度计算很多种,是一个复杂的问题,可以查阅一下。
空间复杂度:O(1)。
稳定性:不稳定。
选择排序
在待排序元素集合中,每次找出一个最小或最大的元素,和待排序元素集合的第一个元素交换数据,直到全部元素排序完。
第一趟找待排序元素集合中最小的元素0,和下标为0的元素交换数据。
第二趟找待排序元素集合中最小的元素1,和下标为1的元素交换数据。
void Swap(int* x, int* y)
{
int t = *x;
*x = *y;
*y = t;
}
// 选择排序
void SelectSort(int* a, int size)
{
// size个元素排序size-1次
for (int i = 0; i < size - 1; ++i)
{
// 从i开始,i前面的已经排序好
int mini = i;
// 找待排序元素集合中最小元素的下标
for (int j = i + 1; j < size; ++j)
{
if (a[j] < a[mini])
mini = j;
}
// 交换数据
Swap(&a[i], &a[mini]);
}
}
时间复杂度:O(N^2)。
空间复杂度:O(1)。
稳定性:不稳定。
选择排序效率非常的低,即使待排序元素集合已经有序,时间复杂度也是O(N^2)。
堆排序
利用堆的性质,从堆顶中选数,大堆的堆顶数据是堆里面最大的,小堆的堆顶数据是堆里面最小的。每次将堆顶的数据和堆的最后一个数据交换,排序好最后一个位置,重复操作,直到堆只剩下一个数据。
// 向下调整算法,排升序建大堆
void AdjustDown(int* a, int size, int parent)
{
int child = parent * 2 + 1;
// 当没有孩子节点的时候,结束
while (child < size)
{
// 找到左右孩子中数据小的那个,先判断是否有右孩子
if (child + 1 < size && a[child + 1] > a[child])
++child;
// 如果孩子节点数据小于父节点数据,交换数据
if (a[child] > a[parent])
{
Swap(&a[child], &a[parent]);
parent = child;
child = parent * 2 + 1;
}
// 孩子节点数据大于等于父节点数据, 结束
else
{
break;
}
}
}
// 堆排序
void HeapSort(int* a, int size)
{
// 建堆
for (int i = (size - 1 - 1) / 2; i >= 0; --i)
{
AdjustDown(a, size, i);
}
// 选堆顶数据排序到最后位置
int end = size - 1;
while (end > 0)
{
Swap(&a[0], &a[end]);
AdjustDown(a, end, 0);
--end;
}
}
时间复杂度:O(N*log(N))。
空间复杂度:O(1)。
稳定性:不稳定。
冒泡排序
依次比较两个相邻元素的大小,如果前面的元素比较大,交换数据,一趟排序排好一个位置,将最大的元素或者最小的元素交换到待排序元素结合的最后一个。
// 冒泡排序
void BubbleSort(int* a, int size)
{
// sz个元素需要排序sz-1趟
for (int i = 0; i < size - 1; ++i)
{
bool flag = true;
// sz个数,排序sz-1次,每次排序一个数,下一趟少排一个数
for (int j = 0; j < size - 1 - i; ++j)
{
// 前一个比后一个,如果前一个大,交换数据
if (a[j] > a[j + 1])
{
flag = false;
int tmp = a[j];
a[j] = a[j + 1];
a[j + 1] = tmp;
}
}
// 如果没有进行交换,已经有序
if (flag)
break;
}
}
时间复杂度:O(N^2)。
空间复杂度:O(1)。
稳定性:稳定。
快速排序
任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止。核心思想:交换数据。
快排有三种实现形式:hoare版本、挖坑法、前后指针法
hoare版本
先选出一个key,一般这个基准值是待排序元素集合的第一个元素(最左边)或者最后一个元素(最右边)。这里选第一个元素做key,用一个变量记录key的下标(因为要交换数据,所以记录的下标)。
定义left是待排序元素集合的第一个元素的下标,定义right是待排序元素集合的最后一个元素下标。左边做key,建议要右边先找(右边先找的好处是,左右相等的位置就是key有序的存储位置)。
排升序,右边找比key值小的元素,左边找比key值大的元素,交换两个元素,重复执行,结束条件是左和右同一个下标,即左等于右,结束后要将key的元素和结束位置的下标的元素交换,一趟排序结束。
一趟排序后key所在的位置就是排序好的位置,因为左边的数据都小于等于key,右边的数据都大于等于key。递归排序左区间和右区间,递归结束条件左大于等于右。
右边找小,左边找大,交换数据。
右边找小,左边找大,交换数据。
右边找小,左边找大,左右相等,结束,交换keyi下标的元素和相遇位置下标的元素,将keyi的位置更新到left位置,递归左区间和右区间。
左区间递归。
左边做key,右边找小,左边找大。
左等于右结束,交换keyi位置的数据和相遇位置下标的元素,更新keyi为left。继续递归左区间和右区间。
// hoare版本
void Hoare(int* a, int begin, int end)
{
// 如果该区间不存在或者只有一个数据,不需要排序
if (begin >= end)
return;
// 左边做key
int keyi = begin;
int left = begin;
int right = end;
// 单趟排序,left等于right结束
while (left < right)
{
// 找小
while (left < right && a[right] >= a[keyi])
--right;
// 找大
while (left < right && a[left] <= a[keyi])
++left;
// 交换数据
Swap(&a[left], &a[right]);
}
// left的位置就是key排序好的位置
Swap(&a[keyi], &a[left]);
// key的位置变为left
keyi = left;
// 递归左区间和右区间
Hoare(a, begin, keyi - 1);
Hoare(a, keyi + 1, end);
}
找小和找大需要注意是大于等于和小于等于。如果大于和小于,排序的时候,第一个元素和最后一个元素相等的时候,会死循环,不会改变left和right。
// 找小
while (left < right && a[right] >= a[keyi])
// 找大
while (left < right && a[left] <= a[keyi])
挖坑法
先选出一个key,待排序元素集合的第一个元素做key,记录key的值和下标。定义left是待排序元素集合的第一个元素的下标,定义right是待排序元素集合的最后一个元素下标。左边做key,建议要右边先找(右边先找的好处是,左右相等的位置就是key有序的存储位置)。
排升序,右边找比key值小的元素,找到后将该元素的值赋值到keyi的位置,更新keyi的位置;左边找比key值大的元素,找到后将该元素的值赋值到keyi的位置,更新keyi的位置;结束条件是左等于右,相遇的位置就是keyi的位置,将key值存储到keyi位置。
右边找小,将找到的值赋值到keyi位置,更新keyi为right。
左边找大,将找到的值赋值到keyi位置,更新keyi为left。
重复执行,左等于右结束,相遇的位置就是keyi的位置,将key存储到keyi位置。一趟排序结束。
一趟排序后key所在的位置就是排序好的位置,因为左边的数据都小于等于key,右边的数据都大于等于key。递归排序左区间和右区间,递归结束条件左大于等于右。
// 挖坑法
void Pit(int* a, int begin, int end)
{
// 递归结束条件
if (begin >= end)
return;
// 记录坑的位置和坑的值
int keyi = begin;
int key = a[keyi];
int left = begin;
int right = end;
// 单趟排序,左等于右结束
while (left < right)
{
// 找小
while (left < right && a[right] >= a[keyi])
--right;
// 将小的值赋值给坑位置
a[keyi] = a[right];
// 小的值的位置变成坑
keyi = right;
// 找大
while (left < right && a[left] <= a[keyi])
++left;
// 将大的值赋值给坑位置
a[keyi] = a[left];
// 大的值的位置变为坑
keyi = left;
}
// 把坑的值存储到坑位置,这个位置就是有序的位置
a[keyi] = key;
Pit(a, begin, keyi - 1);
Pit(a, keyi + 1, end);
}
前后指针法
先选出一个key,待排序元素集合的第一个元素做key,记录key的值和下标。定义slow是待排序元素集合的第一个元素的下标,slow记录的是比key小的最后一个位置,定义fast是待排序元素集合的第二个元素的下标,fast找比key小的数据。
fast位置的数据如果遇到比key小,交换到slow的下一个位置,slow自加1,当fast找完全部数据结束,将key的数据和slow位置上的数据交换,更新keyi。
一趟排序后key所在的位置就是排序好的位置,因为左边的数据都小于key,右边的数据都大于等于key。递归排序左区间和右区间,递归结束条件左大于等于右。
fast遇到比key小的,slow自加一,交换slow位置和fast位置的值。
fast遇到比key小的,slow自加一,交换slow位置和fast位置的值。
fast遇到比key小的,slow自加一,交换slow位置和fast位置的值。
fast走完全部元素,交换keyi位置和slow位置的数据。
一趟排序后key所在的位置就是排序好的位置,因为左边的数据都小于key,右边的数据都大于等于key。递归排序左区间和右区间,递归结束条件左大于等于右。
// 前后指针法
void Pointer(int* a, int begin, int end)
{
// 递归结束条件
if (begin >= end)
return;
int keyi = begin;
// slow 记录的是比key小的最后一个元素,
int slow = begin;
// fast找比key小的,找到小的就交换到slow的下一个位置
int fast = begin + 1;
while (fast <= end)
{
// 如果fast位置的元素比key小,交换到slow的下一个位置
// 如果slow的下一个位置跟fast位置一样,不需要交换
if (a[fast] < a[keyi] && ++slow != fast)
Swap(&a[slow], &a[fast]);
++fast;
}
// slow的位置就是key存储的位置,交换数据
Swap(&a[keyi], &a[slow]);
// 更新keyi
keyi = slow;
// 递归左区间和右区间
Pointer(a, begin, keyi - 1);
Pointer(a, keyi + 1, end);
}
选待排序元素集合的第一个元素作为key,在待排序元素集合本身有序或者接近有序的时候,快排的效率非常慢。需要修改key的选择。
时间复杂度:O(N^2)。时间复杂度高。
空间复杂度:O(N)。递归深度太深,可能会栈溢出,程序崩溃。
稳定性:不稳定。
解决办法:1.随机取key。2.三数取中。
优化
三数取中
左边做key,找待排序元素集合中的中位数,跟第一个元素和最后一个元素比较,找中间大小的值,修改key,key和中间大小的值交换数据。避免选取待排序元素集合中最小值或最大值。
3、8、0的中间大小的值是3,3做key,交换3和key的位置。
// 三数取中
int GetMid(int* a, int begin, int end)
{
// 找中位数
int midi = begin + (end - begin) / 2;
int max = a[begin];
int mid = a[midi];
int min = a[end];
// 将max变为三个数中最大
if (max < mid)
Swap(&max, &mid);
if (max < min)
Swap(&max, &min);
// 将mid变为中间的,min变为最小
if (mid < min)
Swap(&mid, &min);
// 返回mid值对应三个数的下标
if (mid == a[begin])
return begin;
else if (mid == a[midi])
return midi;
else
return end;
}
时间复杂度:O(N*log(N))。
空间复杂度:O(logN)。
小区间优化
小区间递归次数是非常多的。
如果十个数据,每次的key值都是数组中间大小的值,递归展开图。
小区间的递归十分消耗时间,所以将小区间的排序使用别的排序算法,这里建议使用插入排序,插入排队对已经有序和接近有序的效率非常高。
前后指针法的优化。
// 前后指针法
void Pointer(int* a, int begin, int end)
{
// 递归结束条件
if (begin >= end)
return;
// 小区间优化
if (end - begin < 10)
{
SelectSort(a + begin, end - begin + 1);
return;
}
// 左边做key
//int keyi = begin;
// 优化:三数取中
int keyi = begin;
int mid = GetMid(a, begin, end);
Swap(&a[keyi], &a[mid]);
// slow 记录的是比key小的最后一个元素,
int slow = begin;
// fast找比key小的,找到小的就交换到slow的下一个位置
int fast = begin + 1;
while (fast <= end)
{
// 如果fast位置的元素比key小,交换到slow的下一个位置
// 如果slow的下一个位置跟fast位置一样,不需要交换
if (a[fast] < a[keyi] && ++slow != fast)
Swap(&a[slow], &a[fast]);
// 上面那个不能理解看这个
//if (a[fast] < a[keyi])
//{
// ++slow;
// if (slow != fast)
// Swap(&a[slow], &a[fast]);
//}
++fast;
}
// slow的位置就是key存储的位置,交换数据
Swap(&a[keyi], &a[slow]);
// 更新keyi
keyi = slow;
// 递归左区间和右区间
Pointer(a, begin, keyi - 1);
Pointer(a, keyi + 1, end);
}
没有小区间优化
小区间优化
时间复杂度:O(N*log(N))。
空间复杂度:O(logN)~O(N)。
稳定性:不稳定。
快速排序非递归
使用栈数据结构模拟非递归。复制一份之前写的栈的头文件和源文件到当前项目下。函数的参数入栈顺序是从右往左入的。
模拟实现前后指针法快速排序的非递归。
// 快排非递归
void QuickSortNon(int* a, int size)
{
Stack s;
StackInit(&s);
StackPush(&s, size - 1);
StackPush(&s, 0);
// 当栈不为空的时候继续排序
while (!StackEmpty(&s))
{
int left = StackTop(&s);
StackPop(&s);
int right = StackTop(&s);
StackPop(&s);
// 小区间优化
if (right - left < 10)
{
// a+begin是待排序的起始位置,end-begin+1是待排序的个数
SelectSort(a + left, right - left + 1);
continue;
}
// 优化:三数取中
int keyi = left;
int mid = GetMid(a, left, right);
Swap(&a[keyi], &a[mid]);
// slow 记录的是比key小的最后一个元素,
int slow = left;
// fast找比key小的,找到小的就交换到slow的下一个位置
int fast = left + 1;
while (fast <= size - 1)
{
// 如果fast位置的元素比key小,交换到slow的下一个位置
// 如果slow的下一个位置跟fast位置一样,不需要交换
if (a[fast] < a[keyi] && ++slow != fast)
Swap(&a[slow], &a[fast]);
++fast;
}
// slow的位置就是key存储的位置,交换数据
Swap(&a[keyi], &a[slow]);
// 更新keyi
keyi = slow;
// 先入栈右区间,后入栈左区间,先出栈左区间排序
// 如果右区间存在,且不是一个数,入栈
if (keyi + 1 < right)
{
StackPush(&s, right);
StackPush(&s, keyi + 1);
}
// 如果左区间存在,且不是一个数,入栈
if (left < keyi - 1)
{
StackPush(&s, keyi - 1);
StackPush(&s, left);
}
}
StackDestroy(&s);
}
注意小区间优化的return变成了continue。
归并排序
需要申请跟原数组一样的空间大小,归并排序数组到申请空间,将排序好的数据拷贝回数组。将待排序元素集合分为两个区间,递归排序左区间和右区间,左区间和右区间已经有序,将数组数据归并排序申请的空间上,最后拷贝回原数组。递归结束条件是该区间不存在或只有一个数。
// 归并排序需要的子函数
void MergeSortSub(int* a, int* tmp, int begin, int end)
{
// 归并结束条件
if (begin >= end)
return;
// 找中间下标
int mid = begin + (end - begin) / 2;
// 归并左区间和右区间
MergeSortSub(a, tmp, begin, mid);
MergeSortSub(a, tmp, mid + 1, end);
// 左区间和右区间归并结束,拷贝回原数组
int begin1 = begin;
int end1 = mid;
int begin2 = mid + 1;
int end2 = end;
// 左区间和右区间归并
int i = begin;
while (begin1 <= end1 && begin2 <= end2)
{
if (a[begin1] <= a[begin2])
tmp[i++] = a[begin1++];
else
tmp[i++] = a[begin2++];
}
// 左区间或右区间没有归并完
// 剩余数据归并
while (begin1 <= end1)
{
tmp[i++] = a[begin1++];
}
// 剩余数据归并
while (begin2 <= end2)
{
tmp[i++] = a[begin2++];
}
// 拷贝回原数组
for (int j = begin; j <= end; ++j)
{
a[j] = tmp[j];
}
}
// 归并排序
void MergeSort(int* a, int size)
{
int* tmp = (int*)malloc(sizeof(int) * size);
MergeSortSub(a, tmp, 0, size - 1);
free(tmp);
}
时间复杂度:O(N*log(N))。
空间复杂度:O(N)。
稳定性:稳定。
归并排序非递归
归并排序的非递归不好模拟成跟递归的顺序一样。
// 直接模拟
void MergeSortNon(int* a, int size)
{
int* tmp = (int*)malloc(sizeof(int) * size);
// 一(gap)个数据和一(gap)个数据开始归并,因为一个数据本身就是有序的
int gap = 1;
// gap小于size,数组没有归并完
while (gap < size)
{
// 当i<size的时候,还有数据没有归并
for (int i = 0; i < size; i += 2 * gap)
{
int begin1 = i;
int end1 = i + gap - 1;
int begin2 = i + gap;
int end2 = i + 2 * gap - 1;
// 修正边界
if (end1 >= size)
{
end1 = size - 1;
begin2 = size;
end2 = size - 1;
}
else if (begin2 >= size)
{
begin2 = size;
end2 = size - 1;
}
else if (end2 >= size)
{
end2 = size - 1;
}
// 归并两个区间数据
int j = begin1;
while (begin1 <= end1 && begin2 <= end2)
{
if (a[begin1] <= a[begin2])
tmp[j++] = a[begin1++];
else
tmp[j++] = a[begin2++];
}
// 剩余数据归并
while (begin1 <= end1)
{
tmp[j++] = a[begin1++];
}
// 剩余数据归并
while (begin2 <= end2)
{
tmp[j++] = a[begin2++];
}
}
// 归并完拷贝回原数组
for (int k = 0; k < size; ++k)
{
a[k] = tmp[k];
}
// gap*2个数据为一组的已经有序,下次归并gap*2和gap*2个数据归并
gap *= 2;
}
free(tmp);
}