- Java锁的逻辑(结合对象头和ObjectMonitor)
- 还在用饼状图?来瞧瞧这些炫酷的百分比可视化新图形(附代码实现)⛵
- 自动注册实体类到EntityFrameworkCore上下文,并适配ABP及ABPVNext
- 基于Sklearn机器学习代码实战
🔥 排序: 所谓排序,就是使一串记录,按照其中的某个或某些关键字的大小,递增或递减的排列起来的操作。 ✍️ 排序的稳定性: 假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,r[i]=r[j],且r[i]在r[j]之前,而在排序后的序列中,r[i]仍在r[j]之前,则称这种排序算法是稳定的;否则称为不稳定的.
⛅ 基本思想: 把待排序的数逐个插入到一个已经排好序的有序序列中,直到所有的记录插入完为止,得到一个新的有序序列 。
✍️一般地,我们把第一个看作是有序的,所以我们可以从第二个数开始往前插入,使得前两个数是有序的,然后将第三个数插入直到最后一个数插入 。
口头说还是太抽象了,那么我们用一个具体例子来介绍一下吧 。
所以直接插入排序的代码实现如下:
void InsertSort(int* a, int n)
{
int i = 0;
for (i = 0; i < n - 1; i++)
{
int end = i;
//先定义一个变量,将要插入的数保存起来
int x = a[end + 1];
//直到后面的数比前面大的时候就不移动,就直接把这个数放在end的后面
while (end >= 0)
{
if (a[end] > x)
{
a[end + 1] = a[end];
end--;
}
else
{
break;
}
}
a[end + 1] = x;
}
}
⛅ 时间复杂度: 第一趟 end 最多往前移动1次,第二趟是2次……第 n-1 趟是 n-1 次,所以总次数是 1+2+3+……+n-1=n*(n-1)/2 ,所以说时间复杂度是 O(n^2) 。
最好的情况: 顺序 最坏的情况: 逆序 。
⛅ 空间复杂度:由于没有额外开辟空间,所以空间复杂度为O(1) 。
✍️直接插入排序在遇到相同的数时,可以就放在这个数的后面,就可以保持稳定性了,所以说这个排序是 稳定的 .
🐾 基本思想 :希尔排序是建立在直接插入排序之上的一种排序,希尔排序的思想上是把较大的数尽快的移动到后面,把较小的数尽快的移动到后面。先选定一个整数,把待排序文件中所有记录分成个组,所有距离为的记录分在同一组内,并对每一组内的记录进行排序。(直接插入排序的步长为1),这里的步长不为1,而是大于1,我们把步长这个量称为gap,当gap>1时,都是在进行预排序,当gap==1时,进行的是直接插入排序 🔥 同样的我们看图说话! 。
我们先来一个单趟的排序:
int end = 0;
int x = a[end + gap];
while (end >= 0)
{
if (a[end] > x)
{
a[end + gap] = a[end];
end =end - gap;
}
else
{
break;
}
}
a[end + gap] = x;
这里的单趟排序的实现和直接插入排序差不多,只不过是原来是 gap = 1 ,现在是 gap 了。 由于我们要对每一组都进行排序,所以我们可以一组一组地排,像这样:
// gap组
for (int j = 0; j < gap; j++)
{
int i = 0;
for (i = 0; i < n-gap; i+=gap)
{
int end = i;
int x = a[end + gap];
while (end >= 0)
{
if (a[end] > x)
{
a[end + gap] = a[end];
end -= gap;
}
else
{
break;
}
}
a[end + gap] = x;
}
}
也可以对代码进行一些优化,直接一起排序,不要一组一组地,代码如下:
int i = 0;
for (i = 0; i < n - gap; i++)// 一起预排序
{
int end = i;
int x = a[end + gap];
while (end >= 0)
{
if (a[end] > x)
{
a[end + gap] = a[end];
end -= gap;
}
else
{
break;
}
}
a[end + gap] = x;
}
当gap>1时,都是在进行预排序,当gap==1时,进行的是直接插入排序.
所以最后的代码实现如下:
void ShellSort(int* a, int n)
{
int gap = n;
while (gap > 1)// 不要写等于,会导致死循环
{
// gap > 1 预排序
// gap == 1 插入排序
gap /= 2;
int i = 0;
for (i = 0; i < n - gap; i++)// 一起预排序
{
int end = i;
int x = a[end + gap];
while (end >= 0)
{
if (a[end] > x)
{
a[end + gap] = a[end];
end -= gap;
}
else
{
break;
}
}
a[end + gap] = x;
}
}
}
⛅ 时间复杂度 :外层的循环次数,复杂度是 O(logN) 。
每一组的数的个数大概是N/gap,总共有N/n/gap个组,所以调整的次数应该是(1+2+......+N/gap-1)*gap,所以我们分成两种极端来看待这个问题:
当gap很大,也就是gap = N/2的时候,调整的次数是N/2,也就是O(N) 。
当gap很小,也就是gap = 1的时候,按道理来讲调整的次数应该(1+2+......+N-1)*gap,应该是O(n^2),但是这时候应该已经接近有序,次数没有那么多,所以我们不如就看作时间复杂度为O(N) 。
综上:希尔排序的时间复杂度应该是接近O(N*logN) 。
⛅ 空间复杂度 :由于没有额外开辟空间,所以空间复杂度为 O(1) 。
✍️我们可以这样想,相同的数被分到了不同的组,就不能保证原有的顺序了,所以说这个排序是 不稳定的 .
🐾 基本思想 :每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到全部待排序的数据元素排完 。
🔥 同样的我们看图说话! 。
整体排序就是begin往前走,end往后走,相遇就停下,所以整体代码实现如下:
void SelectSort(int* a, int n)
{
int begin = 0;
int end = n - 1;
while (begin < end)
{
int mini = begin;
int maxi = begin;
int i = 0;
for (i = begin; i <= end; i++)
{
if (a[i] > a[maxi])
{
maxi = i;
}
if (a[i] < a[mini])
{
mini = i;
}
// 如果maxi和begin相等的话,要对maxi进行修正
if (maxi == begin)
{
maxi = mini;
}
swap(a[begin], a[mini]);
swap(a[end], a[maxi]);
begin++;
end--;
}
}
}
这里说明一下,其中加了一段修正maxi的代码,就是为了防止begin和maxi相等时,mini与begin交换会导致maxi的位置发生变化,而此时begin就是maxi,若此时交换maxi和end,换到end处的不是最大值,而是最小值mini,所以提前将mini赋值给maxi,当begin与mini交换的时候,mini处就是begin也就是最大值,这样maxi与end交换就不会出现错误 。
⛅ 时间复杂度: 第一趟遍历 n-1 个数,选出两个数,第二趟遍历 n-3 个数,选出两个数……最后一次遍历1个数(n为偶数)或2个数(n为奇数),所以总次数是 n-1+n-3+……+2 ,所以说时间复杂度是 O(n^2) 。
✍️ 最好的情况: O(n^2)(顺序) ✍️ 最坏的情况: O(n^2)(逆序) 。
⛅ 空间复杂度: 由于没有额外开辟空间,所以空间复杂度为 O(1) 。
直接选择排序是 不稳定的 。
举个例子:假设顺序是3 2 3 0,遍历选出最小的0,此时0与3交换,两个3的前后顺序明显发生了变化,所以是不稳定的 。
数据结构初阶--堆排序+TOPK问题 - 一只少年a - 博客园 (cnblogs.com) 。
这篇已经介绍过了 。
补充一点 :堆排序是不稳定的 。
🐾 基本思想: 它重复地走访过要排序的元素列,依次比较两个相邻的元素,如果顺序(如从大到小、首字母从Z到A)错误就把他们交换过来。走访元素的工作是重复地进行直到没有相邻元素需要交换,也就是说该元素列已经排序完成(依次向后比较两个元素,将大的元素放到后面) 。
🔥 同样的我们看图说话! 。
冒泡排序整体代码实现如下:
void BubbleSort(int* a, int n)
{
int i = 0;
//外层循环,需要进行几次排序
for (i = 0; i < n - 1; i++)
{
int j = 0;
//内部循环,比较次数
for (j = 0; j < n - i - 1; j++)
{
if (a[j] > a[j + 1])
{
swap(a[j], a[j + 1]);
}
}
}
}
✍️ 我们思考一个问题,假设当前的序列已经有序了,我们有没有什么办法直接结束排序?就像图中的情况,在第三次排序的时候已经有序,后面的比较是没必要的 。
这当然是有的,我们可以定义一个 exchange 的变量,如果这趟排序发生交换就把这个变量置为1,否则就不变,不发生交换的意思就是该序列已经有序了,利用这样一个变量我们就可以直接结束循环了 。
优化后的冒泡排序代码:
void BubbleSort(int* a, int n)
{
int i = 0;
for (i = 0; i < n - 1; i++)
{
int exchange = 0;
int j = 0;
for (j = 0; j < n - i - 1; j++)
{
if (a[j] > a[j + 1])
{
exchange = 1;
Swap(&a[j], &a[j + 1]);
}
}
// 不发生交换
if (exchange == 0)
break;
}
}
⛅ 时间复杂度: 第一趟最多比较 n-1 次,第二趟最多比较 n-2 次……最后一次最多比较 1 次,所以总次数是 n-1+n-2+……+1 ,所以说时间复杂度是 O(n^2) 。
最好的情况: O(n)(顺序) 最坏的情况: O(n^2)(逆序) 。
⛅ 空间复杂度:由于没有额外开辟空间,所以空间复杂度为O(1) 。
✍️冒泡排序在比较遇到相同的数时,可以不进行交换,这样就保证了稳定性,所以说冒泡排序数 稳定的 .
🐾 基本思想: 通过一趟排序将要排序的数据 分割成独立的两部分 ,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以 递归进行 ,以此达到整个数据变成有序序列 。
✍️ 快速排序的基本流程 。
🐾 基本思想: 任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止.
🔥 同样的我们看图说话! 。
🌰我们要遵循一个原则: 关键词取左,右边先找小再左边找大;关键词取右,左边找先大再右边找小 .
🌰一次过后,2也就来到了排序后的位置,接下来我们就是利用递归来把 key 左边区间和右边的区间递归排好就可以了,如下:
递归左区间:[left, key-1] key 递归右区间:[key+1, right] 。
hoare版本找key值代码实现如下:
int PartSort1(int* a, int left, int right)
{
int key = left;
while (left < right)
{
// 右边找小
while (left < right && a[right] >= a[key])
{
right--;
}
// 左边找大
while (left < right && a[left] <= a[key])
{
left++;
}
Swap(&a[left], &a[right]);
}
Swap(&a[key], &a[left]);
return left;
}
快排代码实现如下:
void QuickSort(int* a, int left, int right)
{
if (left > right)
return;
int div = PartSort1(a, left, right);
// 两个区间 [left, div-1] div [div+1, right]
QuickSort(a, left, div - 1);
QuickSort(a, div + 1, right);
}
✍️我们考虑这样一种情况,当第一个数是最小的时候,顺序的时候会很糟糕,因为每次递归right都要走到头,看下图:
为了优化这里写了一个 三数取中 的代码, 三数取中 就是在序列的首、中和尾三个位置选择第二大的数,然后放在第一个位置,这样就防止了首位不是最小的,这样也就避免了有序情况下,情况也不会太糟糕.
下面是三数取中代码:
int GetMidIndex(int* a, int left, int right)
{
int mid = left + (right - left) / 2;
if (a[mid] > a[left])
{
if (a[right] > a[mid])
{
return mid;
}
// a[right] <= a[mid]
else if (a[left] > a[right])
{
return left;
}
else
{
return right;
}
}
// a[mid] <= a[left]
else
{
if (a[mid] > a[right])
{
return mid;
}
// a[mid] <= a[right]
else if (a[left] > a[right])
{
return right;
}
else
{
return left;
}
}
}
所以加上三数取中优化后的代码如下:
int PartSort1(int* a, int left, int right)
{
int index = GetMidIndex(a, left, right);
Swap(&a[index], &a[left]);
int key = left;
while (left < right)
{
// 右边找小
while (left < right && a[right] >= a[key])
{
right--;
}
// 左边找大
while (left < right && a[left] <= a[key])
{
left++;
}
Swap(&a[left], &a[right]);
}
Swap(&a[key], &a[left]);
return left;
}
🐾 基本思想: 设定一个基准值(一般为序列的最左边元素,也可以是最右边的元素)此时最左边的是一个坑。开辟两个指针,分别指向序列的头结点和尾结点(选取的基准值在左边,则先从右边出发。反之,选取的基准值在右边,则先从左边出发)。 从右指针出发依次遍历序列,如果找到一个值比所选的基准值要小,则将此指针所指的值放在坑里,左指针向前移。 后从左指针出发(选取的基准值在左边,则后从左边出发。反之,选取的基准值在右边,则后从右边出发),依次便利序列,如果找到一个值比所选的基准值要大,则将此指针所指的值放在坑里,右指针向前移。 依次循环步骤,直到左指针和右指针重合时,我们把基准值放入这两个指针重合的位置.
🔥 同样的我们看图说话! 。
挖坑法我们要遵循一个原则: 坑在左,右边找小;坑在右,左边找大 .
挖坑法代码实现如下(加了三数取中算法):
int PartSort2(int* a, int left, int right)
{
int index = GetMidIndex(a, left, right);
Swap(&a[index], &a[left]);
//pivot就是那个坑
int pivot = left;
int key = a[pivot];
while (left < right)
{
// 坑在左边,右边找小
while (left < right && a[right] >= key)
{
right--;
}
Swap(&a[pivot], &a[right]);
pivot = right;
// 坑在右边边,右边找大
while (left < right && a[left] <= key)
{
left++;
}
Swap(&a[pivot], &a[left]);
pivot = left;
}
a[pivot] = key;
return pivot;
}
🐾 基本思想: 前后指针法就是有两个指针 prev 和 cur , cur 个在前, prev 在后, cur 在前面找小,找到了, prev 就往前走一步,然后交换 prev 和 cur 所在位置的值,然后 cur 继续找小,直到cur走到空指针的位置就结束,最后将 prev 的值与 key 交换就完成了一次分割区间的操作 。
🔥 同样的我们看图说话! 。
代码实现:
int PartSort3(int* a, int left, int right)
{
int index = GetMidIndex(a, left, right);
Swap(&a[index], &a[left]);
int key = a[left];
int prev = left;
int cur = left + 1;
while (cur <= right)
{
if (a[cur] < key)
{
prev++;
if (prev != cur)
Swap(&a[cur], &a[prev]);
}
cur++;
}
Swap(&a[prev], &a[left]);
return prev;
}
🐾 小区间优化原理: 当快速排序在递归过程中一直切分区间时,最后会被分成很小的区间,当区间中的数据个数很小时,其实这是已经是没有必要进行再分割的,且最后一层基本上占据了快速排序一半的递归,这是我们可以选择其他的排序来解决这个小区间的排序 。
🐾 还有一个我们要思考的问题就是最后这段小区间用什么排序比较好?
希尔排序适应的是比较多的数据才有优势,堆排序也不行,需要建堆,有点杀鸡用牛刀的感觉,其他三个插入排序、选择排序和冒泡排序相比,还是插入排序比较优,所以我们小区间选择用 插入排序 进行排序 。
void QuickSort(int* a, int left, int right)
{
if (left > right)
return;
int div = PartSort3(a, left, right);
// 两个区间 [left, div-1] div [div+1, right]
if (div - 1 - left > 10)
{
QuickSort(a, left, div - 1);
}
else
{
InsertSort(a + left, (div - 1) - left + 1);
}
if (right - div - 1 > 10)
{
QuickSort(a, div + 1, right);
}
else
{
InsertSort(a + div + 1, right - (div + 1) + 1);
}
}
🐾 基本思想: 利用 栈 来模拟实现递归调用的过程,利用压栈的顺序来实现排序的顺序。 给大家看一个利用栈模拟实现的动图 。
🔥 同样的我们看图说话! 。
我们拿数组arr=[5,2,4,7,9,1,3,6]来举个例子:
第一步:我们先把区间的右边界值7进行压栈,然后把区间的左边界值0进行压栈,那我们取出时就可以先取到左边界值,后取到后边界值 。
第二步:我们获取栈顶元素,先取到0给left,后取到7给right,进行单趟排序 。
第三步:第一趟排完后,区间被分为左子区间和右子区间。为了先处理左边,所以我们先将右子区间压栈,分别压入7和5,然后压入左子区间,3和0 。
第四步:取出0和3进行单趟排序 。
第五步:此时左子区间又被划分为左右两个子区间,但是右子区间只有4一个值,不再压栈,所以只入左子区间,将1和0压栈 。
第六步:取出0和1进行单趟排序 。
第七步:至此,左子区间全部被排完,这时候才可以出5和7排右子区间,是不是很神奇?这个流程其实和递归是一模一样的,顺序也没变,但解决了递归的致命缺陷——栈溢出。后面的流程就不一一展现了 。
void QuickSortNonR(int* a, int left, int right)
{
stack<int> s;
s.push(right);
s.push(left);
while (!s.empty())
{
int newLeft = s.top();
s.pop();
int newRight = s.top();
s.pop();
//挖洞法
int div = PartSort2(a, newLeft, newRight);
// 两个区间 [left, div-1] div [div+1, right]
// 压右区间
if (div + 1 < newRight)
{
s.push(newRight);
s.push(div + 1);
}
// 压左区间
if (newLeft < div - 1)
{
s.push(div - 1);
s.push(newLeft);
}
}
}
⛅ 空间复杂度:
最优的情况下空间复杂度为:O(logN) ;每一次都平分数组的情况 。
最差的情况下空间复杂度为:O( N ) ;退化为冒泡排序的情况 。
⛅ 时间复杂度
快速排序最优的情况下时间复杂度为:O( NlogN ) 。
快速排序最差的情况下时间复杂度为:O( N^2 ) 。
快速排序的平均时间复杂度也是:O(NlogN) 。
快速排序显然是不稳定的,我们试想一下:5 ....5...1....5,这种情况,交换第一个5和1的时候,显然三个5的前后顺序发生了变化,是不稳定的 。
🐾 基本思想: (MERGE-SORT)是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divide andConquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。 🌴 归并条件: 左区间有序 右区间有序 。
🔥 同样的我们看图说话! 。
上半部分递归树为将当前长度为 n 的序列拆分成长度为 n/2 的子序列,下半部分递归树为合并已经排序的子序列 。
再来一张动态图应该更好理解吧~ 。
归并排序代码实现:
void _MergeSort(int* a, int left, int right, int* tmp)
{
if (left >= right)
return;
int mid = left + (right - left) / 2;
// 归并条件:左区间有序 右区间有序
// 如何做到?递归左右区间
// [left, mid] [mid + 1, right]
_MergeSort(a, left, mid, tmp);
_MergeSort(a, mid + 1, right, tmp);
//归并
int begin1 = left;
int end1 = mid;
int begin2 = mid + 1;
int end2 = right;
int i = left;
//对归并的数组进行排序,暂存到tmp数组中
while (begin1 <= end1 && begin2 <= end2)
{
if (a[begin1] < a[begin2])
{
tmp[i++] = a[begin1++];
}
else
{
tmp[i++] = a[begin2++];
}
}
//两个while循环将两个归并数组未加入tmp中的元素加入到tmp当中
while (begin1 <= end1)
{
tmp[i++] = a[begin1++];
}
while (begin2 <= end2)
{
tmp[i++] = a[begin2++];
}
//将tmp数组的值赋值给数组a,因为a是指针,所以对形参进行修改对应实参也会修改
for (i = left; i <= right; i++)
{
a[i] = tmp[i];
}
}
void MergeSort(int* a, int n)
{
int* tmp = (int*)malloc(sizeof(int) * n);
if (tmp == NULL)
{
printf("malloc fail\n");
exit(-1);
}
_MergeSort(a, 0, n - 1, tmp);
free(tmp);
tmp = NULL;
}
⛅ 时间复杂度: O(N*logN) 。
⛅ 空间复杂度: O(N),要来一个临时空间存放归并好的区间的数据 。
归并排序在遇到相同的数时,可以就先将放前一段区间的数,再放后一段区间的数就可以保持稳定性了,所以说这个排序是 稳定的 。
🐾 基本思想: 这里我们用循环来实现这个非递归的归并排序,我们可以先两两一组,在四个四个一组归并…… 。
🔥 同样的我们看图说话!(分两种情况讨论) 。
特殊情况(元素个数为2^i) 。
根据上面这个图,我们可以很快的写出一个框架,例如下面的代码:
void MergeSortNonR(int* a, int n)
{
int* tmp = (int*)malloc(sizeof(int) * n);
if (tmp == NULL)
{
printf("malloc fail\n");
exit(-1);
}
int gap = 1;//每趟合并后序列的长度
while (gap < n)//合并趟数的结束条件是:最后合并后的序列长度>=数组元素的个数
{
int i = 0;
//每趟进行两两合并
for (i = 0; i < n; i += 2 * gap)
{
// [i, i+gap-1] [i+gap, i+2*gap-1]
int begin1 = i;
int end1 = i + gap - 1;
int begin2 = i + gap;
int end2 = i + 2 * gap - 1;
int index = i;
while (begin1 <= end1 && begin2 <= end2)
{
if (a[begin1] < a[begin2])
{
tmp[index++] = a[begin1++];
}
else
{
tmp[index++] = a[begin2++];
}
}
while (begin1 <= end1)
{
tmp[index++] = a[begin1++];
}
while (begin2 <= end2)
{
tmp[index++] = a[begin2++];
}
int j = 0;
for (j = i; j <= end2; j++)
{
a[j] = tmp[j];
}
}
gap *= 2;
}
free(tmp);
tmp = NULL;
}
一般情形(数组的元素个数不一定是2^i ) 。
虽然元素个数不一定是 2^i 个,但是任意元素的个数为n,都必然可以拆写成 2^j+m 个元素的情况 。
由图可知,这种情况下存在两种特殊情况:
我们可以看到这种情况在一次归并中仅存在一次或者零次.
所以调整后的代码如下:
void MergeSortNonR(int* a, int n)
{
int* tmp = (int*)malloc(sizeof(int) * n);
if (tmp == NULL)
{
printf("malloc fail\n");
exit(-1);
}
int gap = 1;
while (gap < n)
{
int i = 0;
for (i = 0; i < n; i += 2 * gap)
{
// [i, i+gap-1] [i+gap, i+2*gap-1]
// 两种需要调整的情况:
// 1.右区间不存在
// 2.正要归并的右区间和左区间长度不一样
int begin1 = i;
int end1 = i + gap - 1;
int begin2 = i + gap;
int end2 = i + 2 * gap - 1;
int index = i;
// 情况1:当右区间不存在的时候,右区间的范围是[begin2,end2],所以begin2越界,就代表着右区间不存在的情况
if (begin2 >= n)
break;
// 情况2,左右区间长度不一,同样此时右区间是存在的,但是end2越界,就代表了左右区间长度不一的情况,此时我们需要做调整
//将end2的长度设置成n-1,将原来的end2(越界)设置成数组a的最后一个元素的位置,因为不平衡的区间最后一个元素一定是数组a的最后一个元素
if (end2 >= n)
end2 = n - 1;
while (begin1 <= end1 && begin2 <= end2)
{
if (a[begin1] < a[begin2])
{
tmp[index++] = a[begin1++];
}
else
{
tmp[index++] = a[begin2++];
}
}
while (begin1 <= end1)
{
tmp[index++] = a[begin1++];
}
while (begin2 <= end2)
{
tmp[index++] = a[begin2++];
}
int j = 0;
for (j = i; j <= end2; j++)
{
a[j] = tmp[j];
}
}
gap *= 2;
}
free(tmp);
tmp = NULL;
}
🌾这样非递归的归并排序就这样被我们实现了。非递归归并排序的实现的难点不在框架,而在边界控制,我们要把 边界控制 的到位,这样就能够很好地实现这个非递归 。
🐾 基本思想: 它的优势在于在对一定范围内的整数排序时,它的复杂度为Ο(n+k)(其中k是整数的范围),快于任何比较排序算法。 当然这是一种牺牲空间换取时间的做法 。
🔥 同样的我们看图说话! 。
我们可以先计数出这个序列数据的范围也就是range = max - min + 1,最大值和最小值都可以通过遍历一遍序列来选出这两个数。然后我们可以开一个大小为range的计数的空间count中,然后将序列中的每一个数都减去min,然后映射到count这个空间中,然后我们再一次取出并加上min依次放进原数组空间中,这样我们就顺利地完成了排序 。
具体代码实现如下:
void CountSort(int* a, int n)
{
int min = a[0];
int max = a[0];
int i = 0;
for (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* count = (int*)malloc(sizeof(int) * range);
if (count == NULL)
{
printf("malloc error\n");
exit(-1);
}
// 初始化开辟的空间
memset(count, 0, sizeof(int) * range);
for (i = 0; i < n; i++)
{
count[a[i] - min]++;
}
int index = 0;
for (int i = 0; i < range; i++)
{
while (count[i]--)
{
a[index++] = i + min;
}
}
free(count);
count = NULL;
}
⛅ 空间复杂度: O(N),要来一个临时空间存放归并好的区间的数据 。
⛅ 时间复杂度: O(MAX(N,范围))(以空间换时间) 。
计数排序在我们这个实现里是 不稳定的 。
排序方法 | 平均情况 | 最好情况 | 最坏情况 | 辅助空间 | 稳定性 |
---|---|---|---|---|---|
直接插入排序 | O(n^2) | O(n) | O(n^2) | O(1) | 稳定 |
希尔排序 | O(nlogn~n^2) | O(n^1.3) | O(n^2) | O(1) | 不稳定 |
直接选择排序 | O(n^2) | O(n^2) | O(n^2) | O(1) | 不稳定 |
堆排序 | O(nlogn) | O(nlogn) | O(nlogn) | O(1) | 不稳定 |
冒泡排序 | O(n^2) | O(n) | O(n^2) | O(1) | 稳定 |
快速排序 | O(nlogn) | O(nlogn) | O(n^2) | O(1) | 不稳定 |
归并排序 | O(nlogn) | O(nlogn) | O(nlogn) | O(n) | 稳定 |
最后此篇关于数据结构高阶--八大排序汇总的文章就讲到这里了,如果你想了解更多关于数据结构高阶--八大排序汇总的内容请搜索CFSDN的文章或继续浏览相关文章,希望大家以后支持我的博客! 。
我是一名优秀的程序员,十分优秀!