目录
[四: HeapSort(堆排序)](#四: HeapSort(堆排序))
接下来的日子会顺顺利利,万事胜意,生活明朗-----------林辞忧
引言
在日常生活当中任何地方都有着排序的思想,对于网购时价格排序,销量排序,好评排序等各种排名,因此对于学习排序算法是很重要,对于排序算法有常见的八种,它们分别是 InsertSort(直接插入排序) ShellSort(希尔排序) BubbleSort(冒泡排序) HeapSort(堆排序) SelectSort(直接选择排序) QuickSort(快速排序) MergeSort(归并排序) CountSort(计数排序) 对于其他的如桶排序,奇数排序等实际用处不大,很少使用。接下来就介绍这八种排序算法
一:InsertSort(直接插入排序)
1.动图
2.思想:把待排序的记录按其关键码值的大小逐个插入到一个已经排好序的有序序列中,直到所有的记录插入完为****止,得到一个新的有序序列
void InsertSort(int* a, int n)
{
//[0,end]有序,插入a[end+1]
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;
}
}
3.时间复杂度:O(N^2) 最坏情况是逆序,最好情况是有序或者接近有序O(N)
二:ShellSort(希尔排序)
1.图片演示
2.思想:
先选定一个整数,把待排序文件中所有记录分成个 组,所有距离为的记录分在同一组内,并对每一组内的记录进行排序。然后,取,重复上述分组和排序的工 作。当到达 =1 时,所有记录在统一组内排好序 。
当gap>1时为预排序,目的是为了接近有序
当gap==1时为直接插入排序
间隔为gap的分为一组,总共gap组
先对一组进行排序
int gap = 3;
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;
}
再对gap组都进行排序
int gap = 3;
for (int j = 0; j < gap; ++j)
{
for (int i = j; 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;
}
}
这样就完成了预排序,但如果对于数据量大的情况下,不止会进行一次预排序且还要控制最后一次预排序的gap==1这样就可以直接排序完成
int gap = n;
while (gap > 1)
{
gap = gap / 3 + 1;
for (int j = 0; j < gap; ++j)
{
for (int i = j; 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;
}
}
}
对于这里可以优化掉一层循环变为
void ShellSort(int* a, int n)
{
//当gap>1时为预排序,目的是为了接近有序
//当gap==1时为直接插入排序
//间隔为gap的分为一组,总共gap组
int gap = n;
while (gap > 1)
{
gap = 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组一次排过去
3.关于gap如何取的问题以及一些其他注意问题
时间复杂度:O(N^1.3)
三:BubbleSort(冒泡排序)
1.动图展示
-
代码实现
void BubbleSort(int* a, int n)
{
for (int i = 0; i < n - 1; ++i)
{
int exchange = 0;
for (int j = 0; j < n - 1 - i; ++j)
{
if (a[j] > a[j + 1])
{
Swap(&a[j],&a[j+1]);
exchange = 1;
}
}
if (exchange == 0)
{
break;
}
}
}
3.时间复杂度O(N^2)
详细介绍可参考https://blog.csdn.net/Miwll/article/details/135315155?spm=1001.2014.3001.5501
四: HeapSort(堆排序)
1.图片展示
2.思想及代码实现
堆排序 (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 = parent * 2 + 1;
}
else
{
break;
}
}
}
void HeapSort(int* a, int n)
{
//排升序建大根堆--向下调整建堆
for (int i = (n - 1 - 1) / 2; i >= 0; --i)
{
AdjustDown(a, n, i);
}
//排数据--首尾交换再向下调整
int end = n - 1;
while (end)
{
Swap(&a[end], &a[0]);
AdjustDown(a, end, 0);
--end;
}
}
时间复杂度为O(N*logN)
详细可以参考https://blog.csdn.net/Miwll/article/details/136636869?spm=1001.2014.3001.5501
五:SelectSort(直接选择排序)
1.动图显示
2.思想及实现
每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到全部待排序的数据元素排完 。
void SelectSort(int* a, int n)
{
//遍历一遍选出最大和最小值下标,再收尾交换
int begin = 0, end = n - 1;
int maxi = 0, mini = 0;
while (begin < end)
{
for (int i = begin; i <= end; ++i)
{
if (a[i] > a[maxi]) maxi = i;
if (a[i] < a[mini]) mini = i;
}
Swap(&a[begin], &a[mini]);
if (begin == maxi)
{
maxi = mini;
}
Swap(&a[end], &a[maxi]);
begin++;
end--;
}
}
这里需要注意当begin和maxi重叠的情况
时间复杂度:O(N^2)
六:QuickSort(快速排序)
1.Hoare版本
1.动图展示
2.思想及实现
对于Hoare版本的快排是选取一个key值,然后先让右边先走找小,再左边找大,再交换继续往后直至相遇,再交换key位置处的值,再以相遇位置为划分子区间继续执行
画一部分递归展开图理解最小子问题的条件
3.为啥相遇位置一定比key值小?---右边先走保证的
4.快排的时间复杂度为O(N*logN),但在有序或者接近有序的情况下最坏为O(N^2),为了防止出现最坏的情况,可以使用三数取中或者随机选key来解决问题
//2.三数取中
int mid = GetMidi(a,begin, end);
Swap(&a[begin], &a[mid]);
int GetMidi(int*a,int left, int right)
{
int mid = (left + right) / 2;
if (a[left] < a[mid])
{
if (a[right] > a[mid])//mid>left right>mid
{
return mid;
}
else if (a[left] > a[right])//mid>left right<mid
{
return left;
}
else
{
return right;
}
}
else //left>mid
{
if (a[right] < a[mid])//right<mid
{
return mid;
}
else if (a[left] > a[right])//right>mid
{
return right;
}
else
{
return left;
}
}
}
5.小区间优化 由于到最后的几步时,递归的深度和广度是非常巨大的,因此可以采用小区间优化的方式减少递归,这里可以采用插入排序
void QuickSort1(int* a, int begin, int end)
{
//最小子问题--区间不存在或者只有一个数据
if (begin >= end) return;
//1.随机选key---选[left, right]区间中的随机数做key
//int randi = rand() % (end - begin + 1);
//randi += begin;//在递归时begin不一定是0开始的
//Swap(&a[begin],&a[randi]);
if (end - begin + 1 < 10)
{
InsertSort(a+begin, end - begin + 1);
}
else
{
//2.三数取中
int mid = GetMidi(a, begin, end);
Swap(&a[begin], &a[mid]);
int keyi = begin;
int left = begin, right = end;
while (left < right)//相遇时就停止
{
//先让右边走
while (left < right && a[right] >= a[keyi])
{
--right;
}
//再左边走
while (left < right && a[left] <= a[keyi])
{
++left;
}
Swap(&a[left], &a[right]);
}
Swap(&a[keyi], &a[left]);
keyi = left;
//[begin,keyi-1] keyi [keyi+1,end]
QuickSort1(a, begin, keyi - 1);
QuickSort1(a, keyi + 1, end);
}
}
2.前后指针版本
1.动图展示
2.思想及实现
当cur处的值>=key时,++cur
当cur处的值<key时,++prev,再交换prev和cur的值,再++cur
void QuickSort2(int* a, int begin, int end)
{
if (begin >= end) return;
int prev = begin;
int cur = begin + 1;
int keyi = begin;
while (cur <= end)
{
if (a[cur] < a[keyi] && ++prev != cur)
{
Swap(&a[prev],&a[cur]);
}
++cur;
}
Swap(&a[prev],&a[keyi]);
keyi = prev;
//[begin,keyi-1]keyi[keyi+1,end]
QuickSort2(a, begin, keyi - 1);
QuickSort2(a, keyi + 1, end);
}
3.非递归版本
对于递归如果深度太深的话,就会导致栈溢出,因此用栈实现非递归版本很重要
思想:走一趟单趟,再右左区间入栈
void QuickSortNonR(int* a, int begin, int end)
{
ST st;
STInit(&st);
//先入右再入左
STPush(&st, end);
STPush(&st, begin);
while (!STEmpty(&st))
{
int left = STTop(&st);
STPop(&st);
int right = STTop(&st);
STPop(&st);
//单趟
int keyi = left;
int cur = left + 1;
int prev = left;
while (cur <= right)
{
if (a[cur] < a[keyi] && ++prev != cur)
Swap(&a[cur],&a[prev]);
++cur;
}
Swap(&a[prev], &a[keyi]);
keyi = prev;
//[left,keyi-1]keyi[keyi+1,right]
//保证入的区间有效
if (keyi + 1 < right)
{
STPush(&st, right);
STPush(&st, keyi + 1);
}
if (left < keyi -1)
{
STPush(&st, keyi - 1);
STPush(&st, left);
}
//Print(a, left, right);
}
STDestroy(&st);
}
4.快排之三路划分
1.快排性能的关键点分析
决定快排性能的关键点是每次单趟排序后,key对数组的分割,如果每次选key基本⼆分居中,那么快 排的递归树就是颗均匀的满⼆叉树,性能最佳。但是实践中虽然不可能每次都是⼆分居中,但是性能 也还是可控的。但是如果出现每次选到最⼩值/最⼤值,划分为0个和N-1的⼦问题时,时间复杂度为 O(N^2),数组序列有序时就会出现这样的问题,我们前⾯已经⽤三数取中或者随机选key解决了这个问 题,也就是说我们解决了绝⼤多数的问题,但是现在还是有⼀些场景没解决(数组中有⼤量重复数据时),即以下情况
此时就提出了采用三路划分的思想来解决
这样再对比key大的数据区间和比key小的数据区间进行递归
//三路划分
void QuickSort3(int* a, int begin, int end)
{
//最小子问题
if (begin >= end) return;
int left = begin, right = end;
int key = a[left];
int cur = left + 1;
while (cur <= right)
{
if (a[cur] < key)
{
Swap(&a[left], &a[cur]);
++cur;
++left;
}
else if(a[cur]>key)
{
Swap(&a[cur],&a[right]);
--right;
}
else
{
++cur;
}
}
//[begin,left-1][left,right][right+1,end]
QuickSort3(a, begin, left - 1);
QuickSort3(a, right+1, end);
}
5.SGI-IntrospectiveSort(自省排序)
introsort是introspectivesort采⽤了缩写,他的名字其实表达了他的实现思路,他的思路就是进⾏⾃ 我侦测和反省,快排递归深度太深(sgistl中使⽤的是深度为2倍排序元素数量的对数值)那就说明在 这种数据序列下,选key出现了问题,性能在快速退化,那么就不要再进⾏快排分割递归了,改换为堆 排序进⾏排序
void IntroSort(int* a, int left, int right, int depth, int defaultDepth)
{
if (left >= right)
return;
// 数组长度⼩于16的⼩数组,换为插入排序,简单递归次数---小区间优化
if (right - left + 1 < 16)
{
InsertSort(a + left, right - left + 1);
return;
}
// 当深度超过2 * logN时改用堆排序
if (depth > defaultDepth)
{
HeapSort(a + left, right - left + 1);
return;
}
depth++;
int begin = left;
int end = right;
// 随机选key
int randi = left + (rand() % (right - left));
Swap(&a[left], &a[randi]);
int prev = left;
int cur = prev + 1;
int keyi = left;
while (cur <= right)
{
if (a[cur] < a[keyi] && ++prev != cur)
{
Swap(&a[prev], &a[cur]);
}
++cur;
}
Swap(&a[prev], &a[keyi]);
keyi = prev;
// [begin, keyi-1] keyi [keyi+1, end]
IntroSort(a, begin, keyi - 1, depth, defaultDepth);
IntroSort(a, keyi + 1, end, depth, defaultDepth);
}
void QuickSort4(int* a, int begin, int end)
{
int depth = 0;
int logn = 0;
int N= end - begin + 1;
for (int i = 1; i < N; i *= 2)
{
logn++;
}
// introspective sort -- 自省排序
IntroSort(a, begin, end, depth, logn * 2);
}
七:MergeSort(归并排序)
1.递归版本
1.动图演示
2.思想及实现
归并排序( MERGE-SORT )是建立在归并操作上的一种有效的排序算法 , 该算法是采用分治法 Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有 序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并
void _MergeSort(int* a, int left,int right,int*tmp)
{
//最小子问题
if (left == right) return;
int mid = (left + right) / 2;
//[begin,mid][mid+1,end]
_MergeSort(a, left, mid, tmp);
_MergeSort(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, sizeof(int) * (right-left+1));
}
void MergeSort(int* a, int n)
{
int* tmp = (int*)malloc(sizeof(int) * n);
if (tmp == NULL)
{
perror("malloc fail\n");
return;
}
_MergeSort(a, 0, n - 1, tmp);
free(tmp);
tmp = NULL;
}
部分递归展开
2.非递归版本
这里的非递归版本采用循环的方式来解决
void MergeSortNonR(int* a, int n)
{
int* tmp = (int*)malloc(sizeof(int) * n);
if (tmp == NULL)
{
perror("malloc fail\n");
return;
}
int gap = 1;
while (gap < n)
{
for (int i = 0; i < n; i += 2 * gap)
{
int begin1 = i, end1 = begin1 + gap - 1;
int begin2 = end1 + 1, end2 = begin2 + gap - 1;
//调整越界问题
if (end1 >= n || begin2 >= n)
{
break;
}
if (end2 >= n)//修正
{
end2 = n - 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, sizeof(int) * (end2-i+1));
}
gap *= 2;
}
}
八:CountSort(计数排序)
1.思想及实现
开辟一个数组用来统计每个数据出现的次数,在相对映射位置的次数++,然后再往原数组写入数据,适合于整形且数据集中的
void CountSort(int* a, int n)
{
int min = a[0];
int max = a[0];
for (int i = 1; i < n; ++i)
{
if (min > a[i]) min = a[i];
if (max < a[i]) max = a[i];
}
int range = max - min + 1;
int* count = (int*)malloc(sizeof(int) * range);
if (count == NULL)
{
perror("malloc fail\n");
return;
}
memset(count, 0, sizeof(int) * range);
//统计次数
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;
}
}
}
总结