一、插入排序
1、直接插入排序
当插入第i(i>=1)个元素时,前面的array[0],array[1],……,array[i-1]已经排好序,此时用array[i]的排序码与array[i-1],array[i-2],……,的排序码顺序进行比较,找到插入位置即将array[i]插入,原来位置上的元素顺序后移。
实现代码:
//插入排序
void InsertSort(int* arr, int len)
{
for (int i = 1; i < len; ++i)
{
int key = arr[i];
int end = i - 1;
while (end >= 0)
{
if (arr[end] > key)
{
arr[end + 1] = arr[end];
end--;
}
else
{
break;
}
}
arr[end + 1] = key;
}
}
直接插入排序特性:
(1)元素集合越接近有序,直接插入排序算法的时间效率越高
(2)时间复杂度:O(N^2)
(3)空间复杂度:O(1)
(4)稳定性:稳定
2、希尔排序(缩小增量排序)
希尔排序又称为缩小增量法。希尔排序的基本思想是:先选定一个整数,把待排序文件中所有元素分成个组,所有距离为gap的元素分在同一组,并对每一组内的记录进行排序。然后,取,重复上述分组和排序的工作。当达到gap=1时,所有元素在统一组内排好序。
实现代码:
//希尔排序
void ShellSort(int* arr, int len)
{
int gap = len;
while (gap > 1)
{
gap = gap / 3 + 1;
for (int i = gap; i < len; ++i)
{
int key = arr[i];
int end = i - gap;
while (end >= 0)
{
if (arr[end] > key) {
arr[end + gap] = arr[end];
end -= gap;
}
else {
break;
}
}
arr[end + gap] = key;
}
}
}
希尔排序特性:
(1)希尔排序是对直接插入排序的优化
(2)当gap>1时都是预排序,目的是让数组更接近于有序。当gap==1时,数组已经接近有序了
(3)希尔排序的时间复杂度不好计算,因为gap的取值方法很多,导致很难去计算,因此希尔排序的时间复杂度不固定。演示代码是按照Knuth提出的方法对gap进行取值的,gap=gap/3+1,时间复杂度约在O(n^1.25)到O(1.6n^1.25)
(4)稳定性:不稳定
二、选择排序
基本思想:每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到全部待排序的数据元素排完。
1、直接选择排序
(1)在元素集合array[i]到array[n-1]中选择关键码最大(小)的数据元素
(2)若它不是这组元素中的最后一个(第一个)元素,则将它与这组元素中的最后一个(第一个)元素交换
(3)在剩余array[i]到array[n-2](array[i+1]到array[n-1])集合中,重复上述步骤,直到集合剩余1个元素
实现代码:
//交换数据
static void _Swap(int* e1, int* e2)
{
int tmp = *e1;
*e1 = *e2;
*e2 = tmp;
}
//选择排序
void SelectSort(int* arr, int len)
{
for (int i = 0; i < len - 1; ++i)
{
int min = i;
for (int j = i + 1; j < len; ++j)
{
if (arr[j] < arr[min])
min = j;
}
if (min != i)
_Swap(&arr[min], &arr[i]);
}
}
直接选择排序特性:
(1)时间复杂度:O(n^2)
(2)空间复杂度:O(1)
(3)稳定性:不稳定
2、堆排序
升序要建大堆,降序要建小堆。
此处为大堆
步骤:
(1)建立堆
(2)交换首元素与尾元素
(3)将首元素向下调整堆
实现代码:
//交换数据
static void _Swap(int* e1, int* e2)
{
int tmp = *e1;
*e1 = *e2;
*e2 = tmp;
}
//向下调整堆
void _AdjustDown(int* arr, int len, int parent)
{
int child = parent * 2 + 1;
while (child < len)
{
if (child + 1 < len && arr[child + 1] > arr[child])
child += 1;
if (arr[child] > arr[parent])
{
_Swap(&arr[child], &arr[parent]);
parent = child;
child = parent * 2 + 1;
}
else
{
return;
}
}
}
//堆排序
void HeapSort(int* arr, int len)
{
//升序,构建大堆
for (int i = (len - 2) / 2; i >= 0; --i)
{
_AdjustDown(arr, len, i);
}
int end = len - 1;
while (end)
{
_Swap(&arr[0], &arr[end]);
_AdjustDown(arr, end, 0);
end--;
}
}
堆排序特性:
(1)时间复杂度:O(n*logn)
(2)空间复杂度:O(1)
(3)稳定性:不稳定
三、交换排序
基本思想:所谓交换,就是根据序列中两个记录键值的比较结果来对换这两个记录在序列中的位置,交换排序的特点是:将键值较大的记录向序列的尾部移动,键值较小的记录向序列的前部移动。
1、冒泡排序
从第一个元素开始,向与后一个元素进行比较,若前一个比后一个元素大,则交换两个值,否则不交换,然后从后一个元素的位置继续与下一个元素进行比较,直到最后。
第一次循环结束后,最后一个元素即最大元素
排序n-1次即可
实现代码:
//交换数据
static void _Swap(int* e1, int* e2)
{
int tmp = *e1;
*e1 = *e2;
*e2 = tmp;
}
//冒泡排序
void BubbleSort(int* arr, int len)
{
for (int i = 0; i < len - 1; ++i)
{
for (int j = 0; j < len - 1 - i; ++j)
{
if (arr[j] > arr[j + 1])
_Swap(&arr[j], &arr[j + 1]);
}
}
}
冒泡排序特性:
(1)时间复杂度:O(n^2)
(2)空间复杂度:O(1)
(3)稳定性:稳定
2、快速排序
基本思想:任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两个子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应的位置上为止。
基准值的选择一般选取首元素或尾元素,但这样选取基准值,但当首元素或尾元素为最大值或最小值时,快速排序事件复杂度为O(n^2),因此应对基准值的选择进行优化。
此处优化采用三值取中法,以降低取到极值的概率。
//三值取中法,降低取到极值的概率
int _GetMidIndex(int* arr, int left, int right)
{
int mid = ((right - left) >> 1) + left;
if (arr[left] > arr[right])
{
if (arr[right] > arr[mid])
return right;
else if (arr[mid] > arr[left])
return left;
return mid;
}
else
{
if (arr[left] > arr[mid])
return left;
else if (arr[mid] > arr[right])
return right;
return mid;
}
}
将区间按照基准值划分为左右两半部分的常见方式:
(1)hoare版本
先选取基准值key(此处key取的最后一个元素的值)。
begin从区间首元素出发,寻找比key值大的元素。end从区间倒数第二个元素出发,寻找比key值小的元素。两个值找到对应元素时,交换begin与end两位置的元素,直到begin==end停止寻找。交换begin位置与区间最后位置的元素。
重复上述步骤,直到排序完成。
实现代码:
//1.hoare版本
int _Partition1(int* arr, int left, int right)
{
int index = _GetMidIndex(arr, left, right - 1);
if (index != right - 1)
_Swap(&arr[index], &arr[right - 1]);
int begin = left;
int end = right - 1;
int key = arr[end];
while (begin < end)
{
while (begin < end && arr[begin] < key)
begin++;
while (begin < end && arr[end] >= key)
end--;
if (begin < end)
_Swap(&arr[begin], &arr[end]);
}
if (begin != right - 1) //防止自身与自身交换
_Swap(&arr[begin], &arr[right - 1]);
return begin;
}
void QuickSort1(int* arr, int left, int right)
{
if (right - left <= 1)
return;
int div = _Partition1(arr, left, right);
QuickSort1(arr, left, div);
QuickSort1(arr, div + 1, right);
}
(2)挖坑法
先取基准值key,形成一个坑位(此处key取的最后一个元素的值)。
begin从区间首元素开始寻找比key值大的元素,找到则将该元素填补到坑位处,该元素位置又成为新的坑位;end位置从区间倒数第二个元素开始寻找比key值小的元素,找到则将该元素填补到坑位处,该元素位置又成为新的坑位。往复循环,直到begin与end遇见为止。此刻将key值填到最后的坑位(begin或end位置处)。一次排序完成。
重复上述步骤,直到排序完成。
实现代码:
//2.挖坑法
int _Partition2(int* arr, int left, int right)
{
int index = _GetMidIndex(arr, left, right - 1);
if (index != right - 1)
_Swap(&arr[index], &arr[right - 1]);
int begin = left;
int end = right - 1;
int key = arr[end];
while (begin < end)
{
while (begin < end && arr[begin] < key)
begin++;
if (begin < end)
{
arr[end] = arr[begin];
end--;
}
while (begin < end && arr[end] >= key)
end--;
if (begin < end)
{
arr[begin] = arr[end];
begin++;
}
}
arr[begin] = key;
return begin;
}
void QuickSort2(int* arr, int left, int right)
{
if (right - left <= 1)
return;
int div = _Partition2(arr, left, right);
QuickSort1(arr, left, div);
QuickSort1(arr, div + 1, right);
}
(3)前后指针法
先取基准值key(此处key取的最后一个元素的值)。
pcur指向首元素位置,pre指向pcur前一个位置。pcur开始遍历,当pcur遇到比key值小的元素时,pcur+1;否则交换pre+1位置与pcur位置的元素(pre+1不等于pcur),交换完pre+1,pcur+1。直到pcur遍历到最后一个元素位置时停止。最后交换pre+1位置与最后一个位置的元素。
重复上述步骤,直到排序完成。
实现代码:
//3.前后指针法
int _Partition3(int* arr, int left, int right)
{
int index = _GetMidIndex(arr, left, right - 1);
if (index != right - 1)
_Swap(&arr[index], &arr[right - 1]);
int pcur = left;
int pre = pcur - 1;
int key = arr[right - 1];
while (pcur < right - 1)
{
if (arr[pcur] > key)
pcur++;
else
{
if (pre + 1 != pcur)
_Swap(&arr[pre + 1], &arr[pcur]);
pre++;
pcur++;
}
}
_Swap(&arr[pre + 1], &arr[right - 1]);
return pre + 1;
}
void QuickSort3(int* arr, int left, int right)
{
if (right - left <= 1)
return;
int div = _Partition3(arr, left, right);
QuickSort1(arr, left, div);
QuickSort1(arr, div + 1, right);
}
小提醒:当快速排序递归到小的子区间是,可以考虑使用插入排序
(4)非递归方法
拆分左右子区间还是采取上述三种方法即可。
快速排序非递归需要利用栈来实现(这里不再说明栈即栈的实现),开始时,先将大区间入栈(即left与right入栈),当栈不为空时,将一对区间从栈中取出来,进行左右子区间拆分,然后分别将左右子区间再入栈。
重复此过程,直到栈空时则排序完毕。
实现代码:
//快速排序非递归实现
void QuickSortNorR(int* arr, int left, int right)
{
Stack s;
StackInit(&s);
StackPush(&s, left);
StackPush(&s, right);
while (!StackEmpty(&s))
{
int r = StackGetTop(&s);
StackPop(&s);
int l = StackGetTop(&s);
StackPop(&s);
if (r - l <= 1)
continue;
int div = _Partition1(arr, l, r);
StackPush(&s, l);
StackPush(&s, div);
StackPush(&s, div + 1);
StackPush(&s, r);
}
StackDestroy(&s);
}
快速排序特性:
(1)快速排序整体的综合性能和使用场景都是比较好的,所以才敢叫快速排序
(2)时间复杂度:O(n*logn)
(3)空间复杂度:O(logn)
递归深度为logn
(4)稳定性:不稳定
四、归并排序
基本思想:归并排序是建立在归并操作上的一种有效的排序算法,该算法是采用分治法的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。
(1)递归方法
实现代码:
//归并排序递归实现
void _merge(int* arr, int left, int mid, int right, int* tmp)
{
int begin1 = left;
int begin2 = mid;
int index = left;
while (begin1 < mid && begin2 < right)
{
if (arr[begin1] <= arr[begin2])
{
tmp[index] = arr[begin1];
index++;
begin1++;
}
else
{
tmp[index] = arr[begin2];
index++;
begin2++;
}
}
while (begin1 < mid)
{
tmp[index] = arr[begin1];
index++;
begin1++;
}
while (begin2 < right)
{
tmp[index] = arr[begin2];
index++;
begin2++;
}
memcpy(arr + left, tmp + left, sizeof(int) * (right - left));
}
void _MergeSort(int* arr, int left, int right, int* tmp)
{
if (right - left <= 1)
return;
int mid = ((right - left) >> 1) + left;
_MergeSort(arr, left, mid, tmp); //左半部分
_MergeSort(arr, mid, right, tmp); //右半部分
_merge(arr, left, mid, right, tmp); //合并
}
void MergeSort(int* arr, int len)
{
int* tmp = (int*)malloc(sizeof(int) * len);
if (tmp == NULL)
{
printf("MergeSort:malloc失败\n");
exit(0);
}
memset(tmp, 0, sizeof(int) * len);
_MergeSort(arr, 0, len, tmp);
free(tmp);
}
(2)非递归方法
定义间隔(gap)为1,利用gap将数组中的元素分组,即每个区间left=i,mid=i+gap,right=i+2*gap
然后对区间排序。排完一遍后,gap乘2,继续分组排序。
直到gap>=len时,停止排序
注意:mid与right取值时,值可能会大于len。这是需要将mid与right置为len。
实现代码:
//归并排序非递归实现
void MergeSortNorR(int* arr, int len)
{
int gap = 1;
int* tmp = (int*)malloc(sizeof(int) * len);
if (tmp == NULL)
{
printf("MergeSortNorR:malloc失败\n");
exit(0);
}
memset(tmp, 0, sizeof(int) * len);
while (gap < len)
{
for (int i = 0; i < len; i += 2 * gap)
{
int left = i;
int mid = i + gap;
int right = i + 2 * gap;
if (mid > len)
mid = len;
if (right > len)
right = len;
_merge(arr, left, mid, right, tmp);
}
gap *= 2;
}
free(tmp);
}
归并排序特性:
(1)归并的缺点在于需要O(n)的空间复杂度,归并排序思考更多的是解决在磁盘中的外排序的问题
(2)时间复杂度:O(n*logn)
(3)空间复杂度:O(n)
(4)稳定性:稳定
五、非比较排序
这里只介绍计数排序。
计数排序又称为鸽巢原理,是对哈希直接定址法的变形应用。
操作步骤:
(1)统计相同元素出现次数
(2)根据统计的结果将序列回收到原来的数组中
若事先未给出数据范围,则应先求出数据范围
实现代码:
//计数排序
void CountSort(int* arr, int len)
{
//若未给出数据具体范围,则应该先求出范围
int minVal = arr[0];
int maxVal = arr[0];
for (int i = 1; i < len; ++i)
{
if (arr[i] < minVal)
minVal = arr[i];
if (arr[i] > maxVal)
maxVal = arr[i];
}
int size = maxVal - minVal + 1;
//申请空间,统计数据出现次数
int* cnt = (int*)malloc(sizeof(int) * size);
if (cnt == NULL)
{
printf("CountSort:malloc失败\n");
exit(0);
}
memset(cnt, 0, sizeof(int) * size);
//统计数据出现次数
for (int i = 0; i < len; ++i)
cnt[arr[i] - minVal]++;
//将排好的数据放回原数组
int index = 0;
for (int i = 0; i < size; ++i)
{
while (cnt[i])
{
arr[index] = i + minVal;
index++;
cnt[i]--;
}
}
free(cnt);
}
计数排序特性:
(1)计数排序在数据范围集中时,效率很高;但是适用范围及场景有限
(2)时间复杂度:O(n)
(3)空间复杂度:O(范围)
(4)稳定性:稳定