#include<stdio.h>
#include<assert.h>
//ϣ
void ShellSort(int* a, int n)
{
assert(a);
int gap = n;
while (gap > 1)//gap==1
{
gap /= 2;
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//a[end]<=x
{
break;
}
}
a[end + gap] = tmp;
}
}
}
int main()
{
int arr[] = { 3,5,2,9,8,10,2,2,9,8 };
int size = sizeof(arr) / sizeof(arr[0]);
ShellSort(arr, size);
}
int left = 0, right = n - 1;
int maxi = left, mini = left;
for (int i = left; i <= right; ++i)//ұ
{
if (a[i] > a[maxi])
{
maxi = i;
}
if (a[i] < a[mini])
{
mini = i;
}
}
swap(&a[left], &a[mini]);
swap(&a[right], &a[maxi]);
剩下只要每次维护left,right即可
但是还有一个坑,如果某一波,maxi正好就在left的位置
那更新最小值的时候就把最大值换走了
所以我们加个特判即可
cpp复制代码
#include<stdio.h.>
void swap(int* a, int* b)
{
int tmp = *a;
*a = *b;
*b = tmp;
}
void SelectSort(int*a,int n)
{
int left = 0, right = n - 1;
while (left < right)
{
int maxi = left, mini = left;
for (int i = left; i <= right; ++i)//ұ
{
if (a[i] > a[maxi])
{
maxi = i;
}
if (a[i] < a[mini])
{
mini = i;
}
}
swap(&a[left], &a[mini]);
if (maxi == left)
{
maxi = mini;
}
swap(&a[right], &a[maxi]);
++left;
--right;
}
}
int main()
{
int arr[] = { 3,5,2,9,8,10,2,2,9,8 };
int size = sizeof(arr) / sizeof(arr[0]);
SelectSort(arr, size);
for (int i = 0; i < size; ++i)
{
printf("%d ", arr[i]);
}
#include<stdio.h>
//swap
void swap(int* px, int* py)
{
int tmp = *px;
*px = *py;
*py = tmp;
}
//向上调整
void AdjustUp(int* a, int child)
{
int parent = (child - 1) / 2;
while (child > 0)//最多调到根
{
if (a[child] > a[parent])
{
swap(&a[child], &a[parent]);
child = parent;
parent = (child - 1) / 2;
}
else
{
break;
}
}
}
//向下调整
void AdjustDown(int* a, const int n, int parent)
{
//找左右孩子大的一个交换
int child = parent * 2 + 1;//suppose左孩子大,经典玩法
while (child < n)//如果孩子超出了数组范围,说明parent是叶子节点
{
if (child + 1 < n && a[child + 1] > a[child])//防止越界
{
child++;
}
if (a[child] > a[parent])
{
swap(&a[parent], &a[child]);
parent = child;
child = parent * 2 + 1;
}
else
{
break;
}
}
}
//排升序--建大堆(向上调整建堆)
void HeapSort1(int* a, int n)
{
for (int i = 1; i < n; ++i)//O(lgN)
{
AdjustUp(a, i);
}
int end = n - 1;
while(end>0)
{
swap(&a[0], &a[end]);
AdjustDown(a, end, 0);
end--;
}
}
//排升序--建大堆(向下调整建堆)
void HeapSort2(int* a, int n)
{
for (int i = (n - 1 - 1) / 2; i >= 0; --i)
{
AdjustDown(a, n, i);
}
int end = n - 1;
while (end > 0)
{
swap(&a[0], &a[end]);
AdjustDown(a, end, 0);
end--;
}
}
int main()
{
int arr[] = { 2,4,5,6,7,2,6 };
const int n = sizeof(arr) / sizeof(arr[0]);
HeapSort2(arr, n);
for (int i = 0; i < n; ++i)
{
printf("%d ", arr[i]);
}
printf("\n");
return 0;
}
打印结果
复杂度分析
关于堆排序的时间复杂度我们也分析过了
如果是向下调整建堆,就是O(n*lgn)
由于是原地建堆,所以空间复杂度是O(1)
五、冒泡排序
冒泡排序属于交换排序
我们来介绍一下交换排序的思想
所谓交换,就是根据序列中的两个记录键值对的比较结果来swap这两个记录在序列的位置
交换排序的特点是:将键值较大的记录先后移动,将键值较小的记录先前移动
冒泡排序也是我们非常熟悉的排序了
这里直接上代码
cpp复制代码
#include<stdio.h>
#include<stdbool.h>
void swap(int* a, int* b)
{
int tmp = *a;
*a = *b;
*b = tmp;
}
void BubbleSort(int* a, int n)
{
for (int i = 0; i < n; ++i)
{
bool flag = false;
for (int j = i + 1; j < n; ++j)
{
if (a[i] > a[j])
{
flag = true;
swap(&a[i], &a[j]);
}
}
if (flag == false)
{
break;
}
}
}
int main()
{
int arr[] = { 3,5,2,9,8,10,2,2,9,8 };
int size = sizeof(arr) / sizeof(arr[0]);
BubbleSort(arr, size);
for (int i = 0; i < size; ++i)
{
printf("%d ", arr[i]);
}
}
打印结果
复杂度分析
显然,如果序列本身有序,遍历一次即可排好序
所以时间复杂度最好情况为O(n)
如果是最坏情况,则是O(n^2)
需要注意的是,在相对有序的情况下,冒泡排序的时间复杂度还是不如插入排序的
空间复杂度是O(1)
六、快排
1 hoare法
单趟:选出一个基准值key,把他放到正确的位置(最终排好序要蹲的事)
例:
最终就会变成
我们有三种方法,第一种是这样的:
法一:
1、具体操作为左边找严格比key大的,右边找严格比key小的,然后swap
2、最后在最后停下的位置和key所在的位置交换一下
一波单趟排序就做好啦~
cpp复制代码
//hoare法
int partition1(int* a, int left, int right)
{
int mid = GetMidNumi(a, left, right);
swap(&a[left], &a[mid]);
int keyi = left;
while (left < right)
{
//左边是key,右边先走
//带上=,表示找严格大/严格小
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[left], &a[keyi]);
return keyi;
}
注:由于是循环套循环,所以做好边界判断
为什么要找严格大于/小于的?
如果某个用例为[2,2,2,2,2,2];那就完了,会一直死循环
2 挖坑法
由于hoare大佬的这个方法太拉拉了,又给出了第二种(感觉更复杂了,意思差不多)
法二:
1、先将key值存在一个变量里,就会形成一个坑位
2、依然是左边找大,右边找小
右边找小,放到坑里,更新坑的位置
左边再找大,放进坑里,更新坑的位置
3、当left和right相遇,将key填入坑中
按图来看,就是这样
cpp复制代码
//挖坑法
int partition2(int* a, int left, int right)
{
int mid = GetMidNumi(a, left, right);
swap(&a[left], &a[mid]);
int key = a[left];
int hole = left;
//此时 left为坑
while (left < right)
{
//左边是key,右边先走
//带上=,表示找严格大/严格小
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;
}
//最后坑的位置放ke
a[left] = key;
return hole;
}
3 前后指针法(recommend)
这个方法还是很清爽很简洁的
法三
1、初始时,prev指向序列开头,cur指针指向prev指针后的一个位置,就像这样
2、cur所在位置比key小的,
如果找到了,就++prev,和cur交换
交换完以后,++cur
3 如果cur所在位置比key大
就++cur
在排序过程中,大概就是两种情况
1 prev紧跟着cur
2 prev和cur隔着比key大的一段数的区间
按图来看,就是这样
cpp复制代码
//前后指针法
int partition3(int* a, int left, int right)
{
int mid = GetMidNumi(a, left, right);
swap(&a[left], &a[mid]);
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[keyi], &a[prev]);
return prev;
}
那么问题来了,我们已经学习了3种单趟排序的方法?
如何实现完整的排序呢?
我们知道,第一波单趟排序排好的是最终key的位置
所以下来的操作非常简单
1 我们对key的左右区间递归使用该函数即可
2 当区间只有一个数/区间不存在时,递归调用结束
有了上一节我们学到的递归的经验,这波函数体就会是这样(以第三种方法为例)
cpp复制代码
void QuickSort(int* a, int left, int right)
{
if (left >= right)
return;
int keyi = PartSort3(a, left, right);
QuickSort(a, left, keyi - 1);
QuickSort(a, keyi+1, right);
}
需要注意的是,三种单趟排序对同一序列的结果可能不同,如果有数据结构题目问
单趟排序的结果,我们需要考虑3种方式
4 非递归
非递归的思路也很简单,只要将递归的逻辑用栈代替即可
1、每次将left、right入栈;
2、每次取两次栈顶组成begin、end调用单趟排序
3、将单趟排序返回的keyi左右两端区间的左右顶点入栈
4、当栈为空时,循环结束
cpp复制代码
//前后指针法
int partition3(int* a, int left, int right)
{
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[keyi], &a[prev]);
return prev;
}
void QuickSortNonR(int* a, int left, int right)
{
stack<int> st;
st.push(left);
st.push(right);
while (!st.empty())
{
int end = st.top();
st.pop();
int begin = st.top();
st.pop();
int keyi = partition3(a, begin, end);
//begin,keyi-1 ; keyi+1,end
if (begin < keyi - 1)
{
st.push(begin);
st.push(keyi-1);
}
if ( keyi + 1<end)
{
st.push(keyi+1);
st.push(end);
}
}
}
int main()
{
int arr[] = { 3,5,2,9,8,10,2,2,9,8 };
int size = sizeof(arr) / sizeof(arr[0]);
QuickSortNonR(arr, 0, size - 1);
for (int i = 0; i < size; ++i)
{
printf("%d ", arr[i]);
}
}
打印结果
5 优化之三数取中(随机选key)
三数取中其实很简单,由于我们固定选最左边的数为key会有不确定性,所以我们选取
左中右三个数中中间的那个为key
cpp复制代码
//三数取中
int GetMidNumi(int* a, int left, int right)
{
int mid = (right + left) / 2;
if (a[left] < a[mid])
{
if (a[mid] < a[right])
{
return mid;
}
//说明mid是最大的
else if (a[right] > a[left])
{
return right;
}
else
{
return left;
}
}
else
{
if (a[mid] > a[right])
{
return mid;
}
//说明mid是最小的
else if (a[right] > a[left])
{
return left;
}
else
{
return right;
}
}
}
6 优化之小区间优化
我们注意到,当递归到一定深度时,每次的区间长度不长
但仍需要递归,这就会导致递归次数太多,有栈溢出的风险
所以我们限制一个长度,在此长度以下,我们直接调插入排序
由于是接近有序,所以效率不会太拉还可以减少递归层数
cpp复制代码
void QuickSort(int* a, int left, int right)
{
if (left >= right)
return;
// 小区间优化--小区间直接使用插入排序
if ((right - left + 1) > 10)
{
int keyi = PartSort3(a, left, right);
QuickSort(a, left, keyi - 1);
QuickSort(a, keyi + 1, right);
}
else
{
InsertSort(a+left, right - left + 1);
}
}
//一把梭哈
void MergeSortNonRS(int* a, int n)
{
int* nums = (int*)malloc(sizeof(int) * n);
int gap = 1;
while (gap < n)
{
for (int i = 0; i < n; i += 2 * gap)
{
//归并
int begin1 = i;
int end1 = i+gap-1;
int begin2 = i+gap;
int end2 = i+2*gap-1;
printf("begin1:%d end1:%d\n", begin1, end1);
printf("begin2:%d end2:%d\n", begin2, end2);
int j = i;
while (begin1 <= end1 && begin2 <= end2)
{
if (a[begin1] < a[begin2])
{
nums[j++] = a[begin1++];
}
else
{
nums[j++] = a[begin2++];
}
}
if (begin1 > end1)
{
while (begin2 <= end2)
{
nums[j++] = a[begin2++];
}
}
if (begin2 > end2)
{
while (begin1 <= end1)
{
nums[j++] = a[begin1++];
}
}
}
//记住加left!
memcpy(a , nums, sizeof(int) *n);
gap *= 2;
}
free(nums);
}
int main()
{
int a[] = { 6,1,2,6,9,3,4,6,10};
MergeSortNonRS(a, 9);
}
由于begin1为i,所以不可能越界
下来,越界有这么几种情况
1、end1越了,不归并了,但是是要拷贝的(因为只剩一个数了)
【8,11】【12,15】
由于我们要一次梭哈拷贝,所以这波我们要修正边界,才能不被覆盖
2、end1没越界,begin2越界了
同理,不用归并,也要修正边界
【8,8】【9,9】
3、只有end2越界了
他是需要归并的,修正end2
【0,7】【8,15】
不归并的修正边界很简单,我们只需修成一个不存在的区间,就不进循环了
而最后需要归并的end2,我们需要计算一下修正到的值,即n-1
cpp复制代码
//一把梭哈
void MergeSortNonRS(int* a, int n)
{
int* nums = (int*)malloc(sizeof(int) * n);
int gap = 1;
while (gap < n)
{
for (int i = 0; i < n; i += 2 * gap)
{
//归并
int begin1 = i;
int end1 = i+gap-1;
int begin2 = i+gap;
int end2 = i+2*gap-1;
printf("begin1:%d end1:%d\n", begin1, end1);
printf("begin2:%d end2:%d\n", begin2, end2);
//修正
if (end1 >= n)
{
end1 = n - 1;
begin2 = n;
end2 = n - 1;
}
else if (begin2 >= n)
{
///修成一个不存在的区间
begin2 = n;
end2 = n - 1;
}
else if (end2 >= n)
{
end2 = n - 1;
}
int j = i;
while (begin1 <= end1 && begin2 <= end2)
{
if (a[begin1] < a[begin2])
{
nums[j++] = a[begin1++];
}
else
{
nums[j++] = a[begin2++];
}
}
if (begin1 > end1)
{
while (begin2 <= end2)
{
nums[j++] = a[begin2++];
}
}
if (begin2 > end2)
{
while (begin1 <= end1)
{
nums[j++] = a[begin1++];
}
}
}
//记住加left!
memcpy(a , nums, sizeof(int) *n);
gap *= 2;
}
free(nums);
}
3 非递归但不梭哈实现法
好消息是,不拷贝,前两个就可以不用修正边界了,直接break出去
只需修正第二个区间的右边界即可
cpp复制代码
//归一部分拷一部分
void MergeSortNonR(int* a, int n)
{
int* nums = (int*)malloc(sizeof(int) * n);
int gap = 1;
while (gap < n)
{
for (int i = 0; i < n; i += 2 * gap)
{
//归并
int begin1 = i;
int end1 = i + gap - 1;
int begin2 = i + gap;
int end2 = i + 2 * gap - 1;
printf("begin1:%d end1:%d\n", begin1, end1);
printf("begin2:%d end2:%d\n", begin2, end2);
//修正
if (end1 >= n || begin2 >= n)
{
break;
}
else if (end2 >= n)
{
end2 = n - 1;
}
int j = i;
while (begin1 <= end1 && begin2 <= end2)
{
if (a[begin1] < a[begin2])
{
nums[j++] = a[begin1++];
}
else
{
nums[j++] = a[begin2++];
}
}
if (begin1 > end1)
{
while (begin2 <= end2)
{
nums[j++] = a[begin2++];
}
}
if (begin2 > end2)
{
while (begin1 <= end1)
{
nums[j++] = a[begin1++];
}
}
memcpy(a +i , nums+i, sizeof(int) * (end2 - i + 1));
}
gap *= 2;
}
free(nums);
}
复杂度分析
归并排序的时间复杂度也是标准的O(n*lgn)
空间复杂度来源于开辟的新数组,为O(n)
归并排序还可以用作外排序,大家感兴趣的话,可以再了解一下~
八、其他排序的介绍
计数排序
计数排序又称鸽巢原理,是对哈希直接定址法的应用
操作为:
1、统计相同元素个数
2、根据统计结果将序列回收回原来的序列
既然是模仿哈希,那么原理是这样的
思考一下元素的大致范围,开一个大的哈希表(数组),通过定址法将带牌序列的元素映射至
哈希表,最后再从哈希表提取元素即可
例:
{6,1,2,1,9,3,3,2,2,8}
我们直接开一个大小为10的数组就够了
然后遍历数组,直接定址,再对应下标值加1即可
遍历之后是这样的
下来就简单,遍历哈希表排序即可
看似很简单的计数排序,我们要考虑一些别的问题
如果序列是这样
{100,101,101,103,109,120},我们再从0开始定址就有些浪费了
所以我们可以统计最大,最小值,就能最大程度的节省空间了
需要注意的是,如果序列含有负数,我们的排序也可以解决
那代码是这样的~
cpp复制代码
void CountSort(int* a, int n)
{
int max = a[0], min = a[0];
for (int i = 1; i < n; ++i)
{
if (a[i] > max)
{
max = a[i];
}
if (a[i] < min)
{
min = a[i];
}
}
int range = max - min + 1;
int* countA = (int*)malloc(sizeof(int) * range);
if (countA == NULL)
{
perror("malloc fail\n");
return;
}
memset(countA, 0, sizeof(int) * range);
// 计数
for (int i = 0; i < n; i++)
{
countA[a[i] - min]++;
}
// 排序
int j = 0;
for (int i = 0; i < range; i++)
{
while (countA[i]--)
{
a[j++] = i + min;
}
}
free(countA);
}
int main()
{
int arr[] = { 2,10,3,90,589,184,505.29,8,83 };
for (auto e : arr)
{
std::cout << e << " ";
}
std::cout << std ::endl;
int sz = sizeof(arr) / sizeof(arr[0]);
CountSort(arr, 0, sz);
for (auto e : arr)
{
std::cout << e << " ";
}
return 0;
}