各种排序的实现
- 排序的概念
- 直接插入排序
- 基本思想
- 实现
- 直接插入排序的特性总结
- 希尔排序
- 基本思想
- 实现
- 希尔排序的特性总结
- 简单选择排序
- 基本思想
- 实现
- 直接选择排序的特性总结
- 堆排序
- 实现
- 堆排序的特性总结
- 冒泡排序
- 基本思想
- 实现
- 冒泡排序的特性总结
- 快速排序
- 基本思想
- hoare版本
- 挖坑法
- 前后指针法
- 快速排序的改进
- 快速排序非递归
- 快速排序的特性总结
- 归并排序
- 基本思想
- 实现
- 归并排序非递归
- 归并排序的特性总结
- 基数排序
- 基本思想
- 实现
- 计数排序
- 基本思想
- 实现
排序的概念
排序:所谓排序,就是使一串记录,按照其中的某个或某些关键字的大小,递增或递减的排列起来的操作。
稳定性:假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,r[i]=r[j],且r[i]在r[j]之前,而在排序后的序列中,r[i]仍在r[j]之前,则称这种排序算法是稳定的;否则称为不稳定的。
内部排序:数据元素全部放在内存中的排序。
外部排序:数据元素太多不能同时放在内存中,根据排序过程的要求不能在内外存之间移动数据的排序。
直接插入排序
基本思想
把待排序的记录按其关键码值的大小逐个插入到一个已经排好序的有序序列中,直到所有的记录插入完为止,得到一个新的有序序列 。
实现
把下标[0 end]看成有序,现在插入end+1,插入之后还要保持[0 end+1]有序
记住,写排序一定要先写单趟排序,在控制写全部的!
//直接插入排序O(N^2)
void InsertSort(int* a, int n)
{
for (int i = 0; i < n - 1; ++i)
{
//[0 end]有序,插入end+1,[0 end+1]保存有序
//一趟
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;
}
}
直接插入排序的特性总结
- 元素集合越接近有序,直接插入排序算法的时间效率越高
- 时间复杂度:O(N^2)
- 空间复杂度:O(1),它是一种稳定的排序算法
- 稳定性:稳定
希尔排序
基本思想
希尔排序法又称缩小增量法。希尔排序法的基本思想是:先选定一个整数gap,把待排序文件中所有记录分成gap组,所有距离为gap的分在同一组内,并对每一组内的记录进行排序。然后,取,重复上述分组和排序的工作。当到达gap==1时,所有记录在统一组内排好序。
希尔排序其实是对直接插入排序的改进,直接插入排序当数据是有序的时候,时间复杂度时O(N),是一个很好的算法。而希尔排序选定一个整数gap,当gap>1的时候预排序,使距离为gap的每组数据进行排序,gap==1的时候,数据基本有序了,然后再执行一次直接插入排序。
实现
把[0 end]看成有序,插入end+gap,使得[0 edd+gap]有序,
写法1
共有gap组,每组N/gap个,每次对每组进行排序
void ShellSort(int* a, int n)
{
//写法1
int gap=n;
//gap>1 预排序
//gap==1 直接插入排序
while (gap > 1)
{
gap /= 2;
//分成gap组
for (int j = 0; j < gap; j++)
{
//每组排序
for (int i = 0; i < n - gap; i += gap)
{
int end = i;
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;
}
}
}
}
写法2
一趟就把每组的都排完
void ShellSort(int* a, int n)
{
//写法2
int gap = n;
while (gap > 1)
{
gap /= 3+1;
for (int i = 0; i < n - gap; i++)
{
int end = i;
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;
}
}
}
gap/2或gap/3+1;都是可以的,都能保证最后一次gap==1,进行直接插入排序。
希尔排序的特性总结
- 希尔排序是对直接插入排序的优化。
- 当gap > 1时都是预排序,目的是让数组更接近于有序。当gap == 1时,数组已经接近有序的了,这样就会很快。这样整体而言,可以达到优化的效果。我们实现后可以进行性能测试的对比。
- 希尔排序的时间复杂度不好计算,因为gap的取值方法很多,导致很难去计算,因此在好些书中给出的希尔排序的时间复杂度都不固定:
《数据结构(C语言版)》— 严蔚敏
《数据结构-用面相对象方法与C++描述》— 殷人昆
因为我们的gap是按照Knuth提出的方式取值的,而且Knuth进行了大量的试验统计,我们暂时就按照:O(n^1.25) 到 O(1.6*n^1.25) 来算。
4. 稳定性:不稳定
简单选择排序
基本思想
每次从待排数据中选择最小(或最大)的数据,放在序列的起始位置,直到所有待排序的数据排完
实现
写法1,(升序)从待排数据里一次选择一个最小的数据。
void SelectSort(int* a, int n)
{
//写法1
for (int i = 0; i < n-1; i++)
{
int mini = i;
for (int j = mini+1; j < n; j++)
{
if (a[j] < a[mini])
{
mini = j;
}
}
Swap(&a[i], &a[mini]);
}
}
写法2,对上面代码的改进,但是时间复杂度没变,从待排数据中选择两个元素,一个最小一个最大。放在数组开头和结点,然后选择次大次小再放。
但是这样写会有一个问题,我下面代码是先找大后找小,然后交换大,再交换小,遇到问题是如果最小的是数组最后一个位置,可以会把mini位置的值覆盖,所有这种情况需要特殊处理。
//直接选择排序
void SelectSort(int* a, int n)
{
//写法2
int begin = 0, end = n - 1;
while (begin < end)
{
int mini = begin, maxi = begin;
for (int i = begin + 1; i <= end; i++)
{
//找大
if (a[i] > a[maxi])
{
maxi = i;
}
//找小
if (a[i] < a[mini])
{
mini = i;
}
}
Swap(&a[end], &a[maxi]);
//如果mini是最后一个位置,就更新mini位置
if (a[mini] == a[end])
mini = maxi;
Swap(&a[begin], &a[mini]);
++begin;
--end;
}
}
直接选择排序的特性总结
- 直接选择排序思考非常好理解,但是效率不是很好。实际中很少使用
- 时间复杂度:O(N^2)
- 空间复杂度:O(1)
- 稳定性:不稳定
堆排序
实现
堆排序所有代码再这里堆的实现,画图和代码分析建堆,堆排序,时间复杂度以及TOP-K问题写的非常清晰,有兴趣的可以看一看。
堆排序的特性总结
- 堆排序使用堆来选数,效率就高了很多。
- 时间复杂度:O(N*logN)
- 空间复杂度:O(1)
- 稳定性:不稳定
冒泡排序
基本思想
基本思想:所谓交换,就是根据序列中两个记录键值的比较结果来对换这两个记录在序列中的位置,交换排序的特点是:将键值较大的记录向序列的尾部移动,键值较小的记录向序列的前部移动。
实现
(升序)两两交换,每次把最大的放在最后面。
首先有n个数,只进行n-1趟排序就行了,其次注意每次把最大的放在最后面,然后下一趟排序就可以把不用和这个数据进行比较。每次都少比较一个数。
void BubbleSort(int* a, int n)
{
for (int j = 0; j < n - 1; j++)
{
int flag = 1;
//一趟
for (int i = 0; i < n - 1-j; i++)
{
if (a[i] > a[i + 1])
{
Swap(&a[i], &a[i + 1]);
flag = 0;
}
}
if (flag == 1)
break;
}
}
冒泡排序的特性总结
- 冒泡排序是一种非常容易理解的排序
- 时间复杂度:O(N^2)
- 空间复杂度:O(1)
- 稳定性:稳定
快速排序
基本思想
其基本思想为:任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止。
hoare版本
(升序)选取左边第一个元素做key,右边先走找小,左边后走找大,找到之后交换,当L和R相撞,再把key和相撞位置的值交换一下。返回相撞的位置,这是一趟排序。然后根据这个相撞返回的地址,把整块区间分成[begin keyi-1] keyi [keyi+1 end],再对左右子区间再次进行排序,当左右子区间有序了,这组数据就全部有序了,因此这是一个递归的过程。
那么有的同学就可能想问了,最后相撞的位置一定是比key小的吗?
有两种情况:
1.L撞R
2.R撞L
上面图就是L撞R,下面我们再看R撞L的情况。
选择左边第一个位置做key,右边先走,左边后走,是一定能保证L和R相撞位置的值小于key;
同样的道理如果右边第一个做key,左边先走————
//hoare版本
int PartSort(int* a, int left, int right)
{
int keyi = left;
while (left < right)
{
//右边先走,找小
while ( a[right] > a[keyi])
{
--right;
}
//左边后走,找大
while ( a[left] <a[keyi])
{
++left;
}
Swap(&a[left], &a[right]);
}
int meeti = left;
Swap(&a[keyi], &a[meeti]);
return meeti;
}
上面代码就对了吗? 我们好好分析一下,修改一下我们的代码。
注意找大找小一定要加上left<right,不然两个就会错过
还得加上=,不然如果出现下面这种情况,可能会死循环
正确代码如下:
//hoare版本
int PartSort(int* a, int left, int right)
{
int mid = GetMidIndex(a, left, right);
Swap(&a[left], &a[mid]);
int keyi = left;
while (left < right)
{
//右边先走,找小
while (left < right && a[right] >= a[keyi])
{
--right;
}
//左边后走,找大
while (left < right && a[left] <= a[keyi])
{
++left;
}
Swap(&a[left], &a[right]);
}
int meeti = left;
Swap(&a[keyi], &a[meeti]);
return meeti;
}
然后根据返回的下标分为左右两个子区间,再次递归
//快速排序
void QuickSort(int* a, int begin, int end)
{
int keyi = PartSort(a, begin, end);
//[0,keyi-1] keyi [keyi+1 ,end]
QuickSort(a, begin, keyi - 1);
QuickSort(a, keyi + 1, end);
}
我们知道递归需要返回条件,不然就会栈溢出。我们画图分析一下递归返回条件。
//快速排序
void QuickSort(int* a, int begin, int end)
{
if (begin >= end)
return;
int keyi = PartSort(a, begin, end);
//[0,keyi-1] keyi [keyi+1 ,end]
QuickSort(a, begin, keyi - 1);
QuickSort(a, keyi + 1, end);
}
这样快递排序hoare版本代码,就全部搞定了,里面有很多坑,一定要注意。
挖坑法
理解了上面的方法,我们再看这种方法。
(升序)把左边第一个元素记录做key,并且当作第一个坑位,右边先走找小,找到比key小的元素把这个元素填到坑位,然后这里就形成了一个新坑位,左边再走找大,找到比key大的元素,再填到坑里。然后形成一个新坑位。重复上述过程,直到L和R相撞,再把key填到相撞的位置,并且把该位置返回。
//挖坑法
int PartSort1(int* a, int left, int right)
{
int key = a[left];
int hole = left;
while (left < right)
{
while (left < right && a[right] >= key)
{
--right;
}
a[hole] = a[right];
hole = right;
while (left < right && a[left] <= key)
{
++left;
}
a[hole] = a[left];
hole = left;
}
a[hole] = key;
return hole;
}
前后指针法
(升序),选取左边第一个元素做key,并且申请两个指针Prev和Cur(在数组里面是下标),Prev初始是指向key位置,Cur初始时指向Prev的下一个位置。当Cur遇到比key小的时候,++Prev,然后Prev和Cur位置的值交换,当Cur遇到比key大的时候,++Cur,直到Cur>right,跳出循环,最后把Prev指向的值和key交换。返回Prev;
//前后指针法
int PartSort2(int* a, int left, int right)
{
int keyi = left;
int prev = left, cur = left + 1;
while (cur <= right)
{
if (a[cur] < a[keyi] && ++prev != cur)
{
Swap(&a[cur], &a[prev]);
}
++cur;
}
Swap(&a[keyi], &a[prev]);
return prev;
}
快速排序的改进
优选Key逻辑
1.随机选择一个位置做key
2.针对有序,选取中间值做key
3.三数取中,第一个位置,中间位置,和最后一个位置选取中间值
但是我们选择第一个位置不就是随机选择的key吗,针对有序处理太麻烦了,所以我们采用第三种方法。
//三数取中
int GetMidIndex(int* a, int left, int right)
{
int mid = left+(right-left)/2;
if (a[left] < a[mid])
{
if (a[mid] < a[right])
return mid;
else if (a[right] < a[left])
return left;
else
return right;
}
else //a[left] >= a[mid]
{
if (a[mid] > a[right])
return mid;
else if (a[right] > a[left])
return left;
else
return right;
}
}
递归到小的区间时,可以考虑使用插入排序
if (end - begin <= 8)
{
InsertSort(a + begin, end - begin + 1);
}
注意直接插入排序a是排序的起始位置,n是要排数据个数。
因为我们每段区间起始是不一样的,要排数据个数也是不一样的 。所以就得上面那种写法。
完整的快速排序代码如下(我们使用的是前后指针版本的代码,换做另两种也是同样的写法)
//三数取中
int GetMidIndex(int* a, int left, int right)
{
int mid = left+(right-left)/2;
if (a[left] < a[mid])
{
if (a[mid] < a[right])
return mid;
else if (a[right] < a[left])
return left;
else
return right;
}
else //a[left] >= a[mid]
{
if (a[mid] > a[right])
return mid;
else if (a[right] > a[left])
return left;
else
return right;
}
}
//前后指针法
int PartSort2(int* a, int left, int right)
{
int mid = GetMidIndex(a, left, right);
Swap(&a[left], &a[mid]);
int keyi = left;
int prev = left, cur = left + 1;
while (cur <= right)
{
if (a[cur] < a[keyi] && ++prev != cur)
{
Swap(&a[cur], &a[prev]);
}
++cur;
}
Swap(&a[keyi], &a[prev]);
return prev;
}
//快速排序
void QuickSort(int* a, int begin, int end)
{
if (begin >= end)
return;
if (end - begin <= 8)
{
InsertSort(a + begin, end - begin + 1);
}
else
{
int keyi = PartSort2(a, begin, end);
//[0,keyi-1] keyi [keyi+1 ,end]
QuickSort(a, begin, keyi - 1);
QuickSort(a, keyi + 1, end);
}
}
快速排序非递归
我们知道内存中的栈区空间太小了,递归太深,容易栈溢出。所以我们自己写一个数据结构种的栈来模拟实现内存中的栈区空间,这个栈是在内存中的堆区申请空间的。
注意看我们递归的时候,递归是什么东西。
其实我们递归的是每段区间,先递归左区间在递归右区间,那么我们来使用栈把递归改成非递归。
//快速排序非递归版本
void QuickSortNonR(int* a, int begin, int end)
{
Stack st;
StackInit(&st);
//整个区间先入栈
StackPush(&st, begin);
StackPush(&st, end);
while (!StackEmpty(&st))
{
int right = StackTop(&st);
StackPop(&st);
int left = StackTop(&st);
StackPop(&st);
//不需要在排
if (left >= right)
continue;
int keyi = PartSort2(a, left, right);
//[left , keyi-1] keyi [keyi+1 ,right]
//右区间先进栈
StackPush(&st, keyi+1);
StackPush(&st, right);
//左区间进栈
StackPush(&st, left);
StackPush(&st, keyi - 1);
}
StackDestroy(&st);
}
快速排序的特性总结
- 快速排序整体的综合性能和使用场景都是比较好的,所以才敢叫快速排序
- 时间复杂度:O(N*logN)
- 空间复杂度:O(logN)
- 稳定性:不稳定
归并排序
基本思想
归并排序(MERGE-SORT)是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divide and
Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有
序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。
实现
我们画图分析一下归并的核心操作
由于我们需要额外一个数组,所以写一个递归子函数,不然就重复申请数组了。
void _MergerSort(int* a, int left, int right,int* tmp)
{
//分解返回条件
if (left >= right)
return;
//分解
int mid = (left + right) / 2;
//[left mid] [mid+1 right]
_MergerSort(a,left, mid ,tmp);
_MergerSort(a, mid + 1, right, tmp);
//合并
int begin1 = left, end1 = mid;
int begin2 = mid + 1, end2 = right;
int i = left;
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++];
}
//把数据拷贝回原数组
memcpy(a + left, tmp + left, (right - left + 1) * sizeof(int));
}
//归并排序
void MergerSort(int* a, int n)
{
int* tmp = (int*)malloc(n * sizeof(int));
if (tmp == NULL)
{
perror("malloc fail");
return;
}
_MergerSort(a, 0, n - 1,tmp);
}
归并排序非递归
//归并排序非递归
void MergerSortNonR(int* a, int n)
{
int* tmp = (int*)malloc(n * sizeof(int));
if (tmp == NULL)
{
perror("malloc fail");
return;
}
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 j = i;
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++];
}
//拷贝回原数组
memcpy(a + i, tmp + i, (end2 - i + 1) * sizeof(int));
}
gap *= 2;
}
free(tmp);
tmp = NULL;
}
我们的归并排序已经写好一大半了,但是还有一些问题,
数据个数不一定是整数倍,计算直接是按照整数倍计算的,存在越界需要修正一下
针对三种越界情况对代码修改一下
int gap = 1;
while (gap < n)
{
//每组gap个数据归并
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 j = i;
//第一组end1越界
if (end1 >= n)
{
break;
}
//第二组越界全部越界
if (begin2 >= n)
{
break;
}
//第二组越界部分越界,修改end2区间
if (end2 >= n)
{
end2 = n - 1;
}
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++];
}
//拷贝回原数组
memcpy(a + i, tmp + i, (end2 - i + 1) * sizeof(int));
}
printf("\n");
gap *= 2;
}
free(tmp);
tmp = NULL;
}
还得注意不能一趟完成之后在拷贝回原数组,需要到那一块就把那一块拷贝回去,因为tmp里面是随机数。
归并排序的特性总结
- 归并的缺点在于需要O(N)的空间复杂度,归并排序的思考更多的是解决在磁盘中的外排序问题。
- 时间复杂度:O(N*logN)
- 空间复杂度:O(N)
- 稳定性:稳定
基数排序
基本思想
实现
基数排序回收数据一个先进先出的思想,因此我们就可以使用队列。一共要使用10个队列。那申请一个队列数组,由于还没有学到C++因此下面是用C语言实现的。关于对队列有问题的可以看这个队列的实现
#define QENumber 10
typedef struct QArray
{
QE qe[QENumber];
}QArray;
QArray Qarray;
//获得每趟要排序的数字
int GetKey(int value, int k)
{
int key = 0;
while (k >= 0)
{
key = value % 10;
value /= 10;
--k;
}
return key;
}
//分发数据
void Distribute(int* a, int n, int k)
{
for (int i = 0; i < n; ++i)
{
int key = GetKey(a[i], k);
//进对应的队列
QueuePush(&(Qarray.qe[key]), a[i]);
}
}
//回收数据
void Collect(int* a)
{
int j = 0;
for (int i = 0; i < QENumber; ++i)
{
while (!QueueEmpty(&(Qarray.qe[i])))
{
a[j++] = QueueFront(&(Qarray.qe[i]));
QueuePop(&(Qarray.qe[i]));
}
}
}
//基数排序
void RadixSort(int* a, int n)
{
//初始化QENumber个队列
for (int i = 0; i < QENumber; i++)
{
QueueInit(&(Qarray.qe[i]));
}
//找最大数,看需要排几趟
int max = a[0];
for (int i = 1; i < n; i++)
{
if (a[i] > max)
{
max = a[i];
}
}
int k = 0;
while (max)
{
++k;
max = max / 10;
}
//排k趟
for (int i = 0; i < k; ++i)
{
//分发数据
Distribute(a,n,i);
//回收数据
Collect(a);
}
//销毁队列
for (int i = 0; i < QENumber; ++i)
{
QueueDestroy(&(Qarray.qe[i]));
}
}
计数排序
基本思想
思想:计数排序又称为鸽巢原理,是对哈希直接定址法的变形应用。 操作步骤:
- 统计相同元素出现次数
- 根据统计的结果将序列回收到原来的序列中
但是如果是下面这种情况
因此我们进行映射的时候,为了避免浪费,采取相对映射。
实现
//计数排序
//时间复杂度O(N+range)
//空间复杂度O(rang)
void CountSort(int* a, int n)
{
//相对映射
int min = a[0], max = a[0];
for (int i = 1; i < n; i++)
{
if (a[i] < min)
{
min = a[i];
}
if(a[i] > max)
{
max = a[i];
}
}
int range = max - min + 1;
int* count = (int*)calloc(range, sizeof(int));
if (count == NULL)
{
perror("malloc fail\n");
return;
}
//统计每个数出现的次数
for (int i = 0; i < n; i++)
{
count[a[i]-min]++;
}
//排序
int j = 0;
for (int i = 0; i < range; i++)
{
while (count[i]--)
{
a[j++] = i+min;
}
}
}