数据结构初阶·排序算法(内排序)

目录

前言:

1 冒泡排序

2 选择排序

3 插入排序

4 希尔排序

5 快速排序

5.1 Hoare版本

5.2 挖坑法

5.3 前后指针法

5.4 非递归快排

6 归并排序

6.1递归版本归并

6.2 非递归版本归并

7 计数排序

8 排序总结


前言:

目前常见的排序算法有9种,冒泡排序,选择排序,插入排序,希尔排序,快速排序,归并排序,计数排序,基数排序,桶排序。实际生活中排序的应用也是有限的,今天我们介绍其中7个,基数排序和桶排序不介绍。介绍常用的即可。

顺带一嘴,本文里面所有的排序都是内排序,也就是在内存里面进行排序的,还有一种排序叫做外排序,即是在磁盘里面进行排序的,这种排序具有记忆性,外排序用到的就是归并排序,因为归并排序有一个特点就是空间复杂度为O(N)。

好了正文开始。


1 冒泡排序

这个排序是排序中的老大哥了,不是说效率有多高,是因为一开始接触排序的同学绝对逃不开它哈哈。

冒泡排序的思想是两两比较,从而达到冒泡一趟可以确定一个数的位置,所以一趟下来需要的操作有两两比较,一趟就需要遍历整个数组,所以想要确定数组中所有元素的位置就需要两个循环,一个循环用来控制趟数,一趟循环用来控制两两比较。

我们排升序,一旦两两相邻的数满足条件,就交换两个变量的位置,基本思想确定了,现在来实现基本代码:

int arr[] = { 5,3,1,4,6,8,2,9,0,7 };
//冒泡排序
for (int i = 0; i < sizeof(arr) / sizeof(int); i++)
{for (int j = 0; j < sizeof(arr) / sizeof(int) - 1; j++){if (arr[j] > arr[j + 1]){swap(arr[j], arr[j + 1]);}}
}

这就是最最基本的代码了,注意,这里的j < sizeof(arr) / sizeof(int) - 1的减一是一定要的,因为不减一是可能越界的,比如最后一个数不用冒泡了,但是没有减一就又冒了过去, + 1之后一下就越界了。

那么,我们在此基础上面如何进行修改呢?

我们知道,冒泡一趟可以确定一个数的位置,如果要排序10个数的位置,那么有没有必要冒泡10趟?没有吧,因为确定了9个数的位置最后一个数的位置自然而然就确定了,所以趟数可以进行修改,再来看第二个循环,一趟下来就确定了一个数的位置,那么最后1个或者n个数就不用冒泡了,因为我们知道它是已经排序好了的,那么一趟排好一个,我们就可以少冒泡一个,所以可以利用i进行循环次数的减少。

	for (int i = 0; i < sizeof(arr) / sizeof(int) - 1; i++){for (int j = 0; j < sizeof(arr) / sizeof(int) - i - 1; j++){if (arr[j] > arr[j + 1]){swap(arr[j], arr[j + 1]);}}}

那么我们还有没有修正空间呢?

有的,如果我们排序一个升序数组,但是该数组本身就是一个升序的数组,我们冒泡完一趟发现所有数都满足升序,就不想走后面的怎么办?这个时候我们就应该使用标志来确定一趟有没有交换,如果一趟没有交换,我们也就没有必要冒泡下去了:

	for (int i = 0; i < sizeof(arr) / sizeof(int) - 1; i++){int flag = 0;for (int j = 0; j < sizeof(arr) / sizeof(int) - i - 1; j++){if (arr[j] > arr[j + 1]){swap(arr[j], arr[j + 1]);flag = 1;}}if (0 == flag)break;}

这才是冒泡的最终形态,那么简单分析一下:
循环次数是一个标准的等差数列,所以时间复杂度为O(N^2);

空间复杂度方面,是没有新开空间的,所以空间复杂度为O(1)。


2 选择排序

选择排序顾名思义,通过选择来进行排序,我们选择的是最大最小值,再选择次大次小值,给他们安放到正确的位置上,从而完成排序:

这是选择排序的动图,是选择的最小值,但是我们今天实现的比这个高级一点点,我们今天实现的是双向选择,同时选择最大的和最小的,相对上面的来说并没有难度的增加,无非是代码加了一点而已。

选择排序的总体思想就是两边缩短,从现有的可访问区间里面选择最值,那么我们就需要控制区间,剩下的就是交换和选择最值了:

void SelectSort(int* arr,size_t size)
{int begin = 0, end = size - 1;while (begin  <  end){int mini = begin;int maxi = begin;//选择排序选择是区间 ->循环条件是区间for (int i = begin + 1; i <= end; i++){//找到最小的 记录下标if (arr[i] < arr[mini]){mini = i;}//找到最大的 记录下标if (arr[i] > arr[maxi]){maxi = i;}}swap(arr[begin], arr[mini]);//当max的值在最前面的时候 ->交换前已经冲突if (maxi == begin)maxi = mini;swap(arr[end], arr[maxi]);begin++, end--;}
}

这里的begin和end是左闭右闭区间,所以在循环控制的时候就需要注意是i <= end,同时,我们相当于是在1 到下标为size - 1的数据,这里要是想要用i = begin开始遍历也可以,无非就是加了一条让自己和自己比较的语句,实际没必要,能减少循环次数咱们就减少。

那么有一个新问题,如果数组的最大值在数组下标为0的位置,那么最小元素就和最大值发生了交换,此时最大值的下标已经发生改变,我们再进行一轮的交换就不行,所以这里需要额外判断一下,防止maxi的值交换前变化。

时间复杂度方面,是一个标准的等差数列,也就是O(N^2),空间复杂度方面,没有多开空间,所以是O(1)。


3 插入排序

如果所示,是插入排序的动态演示。

单趟来看,就是把第n个数看成要插入的数据,再分别是前n - 1个数据进行比较,如果满足插入条件,就插入到相应位置,这里就需要一个临时变量来存储插入的数据,并且存在数据覆盖,那么整体来看,就是插入一个数据,把该数据之前的所有数据看成是有序的,即我们应该从前两个数据开始操作。

int end;
int tmp = arr[end + 1];
while (end >= 0)
{if (arr[end] > tmp){arr[end + 1] = arr[end];end--;}else{break;}
}
arr[end + 1] = tmp;

这就是一个单趟,end + 1为插入数据,前end个数据都是要比较的元素,end >= 0也是因为下标为0的元素也需要比较。

那么插入排序就可以理解为,排序 [0,1],[0,2],[0,3]……[0,size- 1]区间,所以还需要一个循环用来控制end,好保证是区间一个一个排号的:

for (int i = 0; i < size - 1; i++)
{int end = i;int tmp = arr[end + 1];while (end >= 0){if (arr[end] > tmp){arr[end + 1] = arr[end];end--;}else{break;}}arr[end + 1] = tmp;
}

这就是一个标准的插入排序,这个排序理解好之后,希尔排序才好理解,因为希尔就是在该排序的基础上进行修正的。

那么这个的时间复杂度和空间复杂度都很标准,是O(N^2),O(1)。


4 希尔排序

可以看到这里的动图和之前的动图有着不一样的点,即颜色不同,这里的颜色不同代表的是分的组别不同,即希尔将不同的元素按照一定的gap(间隔)分成了不同的组,那么问题来了,为什么要在插入排序的基础上加上分组这个思想呢?

原因是插入排序的最不理想的时候就是完全倒序的时候,这个时候基本上把循环吃满了,所以希尔就想,能不能先预排序一下,把所有的元素尽量的靠近最终的位置,所以分组排序,是一个预排序的过程,让数据尽量接近正确位置,预排序我们需要用到gap,即间隔,我们将间隔了gap的数据分为同一组,这一组数据里面进行排序,gap在排序的过程也在不断变化,那么为什么要变化呢?

这样想,gap = 1就是插入排序,就会好理解很多了。

所以现在我们要解决的第一个问题是,如何预排序,假设gap = 4,我们联想插入排序的时候,是gap = 1的排,那么现在无非就是间隔大了一点,所以只需略加修改:

int gap = 4;
int end = 0;
int tmp = arr[end + gap];
while (end >= 0)
{if (arr[end] > tmp){arr[end + gap] = arr[end];end -= gap;}else{break;}
}
arr[end + gap] = tmp;

这是预排序了一组,现在就是,加循环,使每个组都预排序了,整个预排序就算是完成了:

	for (int i = 0; i < gap; i++){for (int j = 0; j < size - gap; j++){int end = j;int tmp = arr[end + gap];while (end >= 0){if (arr[end] > tmp){arr[end + gap] = arr[end];end -= gap;}else{break;}}arr[end + gap] = tmp;}}

这是最原始版本的预排序,是不是三层循环看着头都大了?最外层循环用来控制排序哪一组的,第二层循环,用来控制数组中该组的所有数据,内层就是进行排序,这种排序就太老实了,看着头大。

现在不难发现,gap不止是间隔,gap实际上是把所有的数据分为多少组,反正都是组内部相互排序了,我们每个组同时排不可以吗?

int gap = 4;
for (int i = 0; i < size - gap; i++)
{int end = i;int tmp = arr[end + gap];while (end >= 0){if (arr[end] > tmp){arr[end + gap] = arr[end];end -= gap;}else{break;}}arr[end + gap] = tmp;
}

画一下图的话就会发现相当于是每个大区间的小区间依次排序,这样看着就不乱,那么预排序现在就做好了,我们现在就是考虑怎么从预排序变成最终排序?

那就很简单了,gap等于1就是最终排序,但是我们的gap也不能等于0,而且这个gap说法有挺多的,最后大部分结论是三等分数组,但是为了gap不能等于0,就会除3之后 + 1,只要让gap最终走到1但是不为0就可以:

int gap = size;
while (gap > 1)
{gap = gap / 3 + 1;for (int i = 0; i < size - gap; i++){int end = i;int tmp = arr[end + gap];while (end >= 0){if (arr[end] > tmp){arr[end + gap] = arr[end];end -= gap;}else{break;}}arr[end + gap] = tmp;}
}

最终版本如上。

关于时间复杂度是很难计算的,主要是因为gap,这里就记住结论,O(N^1.3),空间复杂度是O(1)。


5 堆排序

堆排序就是老生常谈的知识了,因为向上建堆的时间复杂度是O(N * logN),向下建堆的时间复杂度是O(N - logN)即O(N),我们就采用向下建堆,最后呢,调整一下就没问题了,先来向下调整算法:

void AdjustDown(int* a, int size, int parent)
{int child = parent * 2 + 1;while (child < size){if (child + 1 < size && a[child + 1] > a[child]){++child;}if (a[child] > a[parent]){swap(a[child], a[parent]);parent = child;child = parent * 2 + 1;}else{break;}}
}

然后开始建堆,建堆的思想很简单,如果每个堆都是很大堆,那么整个堆就是大堆了,所以我们从最后一个父节点开始调整,就有(size - 1 - 1 / 2)来找第一个父节点,随机调整每一个父节点:

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;
}

时间复杂度标准的O(N * logN),空间复杂度为O(1);


6 快速排序

快速排序最初是由hoare发明的,简称为快排,既然敢叫快排就有它的厉害之处,快排的整体思想是将一个数一趟放到正确的位置,简单来说就是一趟下来,该数左边的数一定小于它,右边的数一定大于它,这里我们看个动图:

好吧没找到哈哈哈

那我描述一下,hoare版本是将左边第一个数据,即下标为0的元素选为key,然后有两个“东东”分别从首元素和末尾元素出发,因为我们排的是升序,所以精髓是在于右边的小东东先走,左东东找大,右东东找大小,直到右边的找到了比key小的就停下来,左边同理,找到比key大的就停下来,当两个都找到了,ok进行一次交换,这样相当于把一大一小换位,最后左右东东相遇的时候,就是key的正确位置,因为左右东东已经交换了来的路上的所有比key大的和比key小的,这样一趟下来,key的位置就是正确位置。

一趟下来我们就能确定一个数的正确位置,此时,数组被分为了三段,[begin,key - 1] key [key + 1,end],如果我们对最右边的区间在走一个单趟,还是会被分为三个区间,所以这里我们要用到的还有递归,遍历了根之后,然后遍历左子树右子树(二叉树乱入),这里就和二叉树挺像的了,这里其实就是前序遍历了。那么现在就来实现一下hoare版本吧。

6.1 Hoare版本

void QuickS(int* arr,int begin,int end)
{if (begin >= end)return;int keyi = begin;int left = begin, right = end;while (left < right){//右边找小while (left < right && arr[right] >= arr[keyi]){right--;}//左边找大while (left < right && arr[left] <= arr[keyi]){left++;}swap(arr[right], arr[left]);}swap(arr[keyi], arr[left]);keyi = left;QuickS(arr, begin, keyi - 1);QuickS(arr, keyi + 1, end);}

霍尔版本就需要三个参数,因为我们对区间不停的排序,所以是arr,begin,end,然后对于为什么要赋值给left 和 right呢?因为最后的递归调用需要用到原来的begin和end,所以这里需要临时变量存储一下,当然有其他的存储方法,都是可以的。

然后是循环条件,最外while循环的循环条件没什么好说的,里层的while的循环条件是比较容易忘记的,因为里层的while循环条件也是要有left < left的,如果没有,那么一旦找不到就会死命的找,就会导致left > right的情况,其次,>= <= 的等于也是必要的,如果没有等于,left right 就会停在那里,导致原来要找的小的或者大的没有找到。

找到进行两次交换,最后就是两次区间递归,这里只要保证区间是对的,变量怎么取都可以,只要保证的是递归的那两个区间就行。

那么这里,提问为什么一定要右边先走?

因为要保证最后和arr[keyi]交换的位置是比keyi小的。

为什么右边相遇的位置一定是比arr[keyi]小的?

是因为右边先走,右边先走有两种相遇情况。
第一种是R遇见L,R没有找到小的,一直往L走,在此之前可能是R L都动过,所以L此时下面是一个比keyi小的数,相遇一定是比keyi小的,也可能是L没动过,那么R就可能是一直找到keyi自己,也是一种正确的情况。

第二种是L遇见R,R已经找到了小的,所以此时L遇见了R也肯定是遇见了一个比arr[keyi]小的数。

以上是快排的大部分易错点,后面的三个版本都是在单趟的基础上进行优化,我们就将单趟实现的不同函数分别写,最后调用函数,再递归,如下:

int PartSort1(int* arr, int begin, int end)
{//三数取中int midi = GetMidi(arr, begin, end);swap(arr[begin], arr[midi]);int left = begin, right = end;int key = begin;while (left < right){//右边找小while (arr[right] >= arr[key] && left < right){right--;}//左边找大while (arr[left] <= arr[key] && left < right){left++;}swap(arr[left], arr[right]);}swap(arr[left], arr[key]);return left;
}
void QuickSort(int* arr, int begin, int end)
{if (begin >= end)return;int key = PartSort1(arr, begin, end);QuickSort(arr, begin, key - 1);QuickSort(arr, key + 1, end);}

这里可能还有一个疑问就是为什么keyi一定要取左边的,我取右边的不行吗?取两边都是可以的,取中间也可以,但是中间就麻烦一点,有的时候固执的取左边效率就比较低下,比如倒序的情况,这里的话我们就需要三数取中,也就是在左右中三个数取中间值的数来当keyi,这样好点,就和希尔的预排一样,有点类似,希尔是把数提前接近,这里是选择一个不用执行那么多语句的数来当keyi。三数取中就比较就可以了:

int GetMidi(int* a, int begin, int end)
{int midi = (begin + end) / 2;// begin midi end 三个数选中位数if (a[begin] < a[midi]){if (a[midi] < a[end])return midi;else if (a[begin] > a[end])return begin;elsereturn end;}else // a[begin] > a[midi]{if (a[midi] > a[end])return midi;else if (a[begin] < a[end])return begin;elsereturn end;}
}

但是但是,现在还有一个小问题就是,有的时候也存在杀鸡用牛刀的情况,比如我要对10000个数进行快排,剩余最后几个数的时候,我还是要一个个的去走递归,那就太麻烦了,我还不如选一个排序来完成接下来的排序呢,比如7个数我要递归7次,还是有点麻烦的,所以有的时候会这样:

//小区间优化版本 
void QuickSort1(int* arr, int begin, int end)
{if (begin >= end)return;if (end - begin + 1 <= 10){//+begin是必要的InsertSort(arr + begin, end - begin + 1);}else{int midi = GetMidi(arr, begin, end);swap(arr[begin], arr[midi]);int left = begin, right = end;int key = begin;while (left < right){//右边找小while (arr[right] >= arr[key] && left < right){right--;}//左边找大while (arr[left] <= arr[key] && left < right){left++;}swap(arr[left], arr[right]);}swap(arr[left], arr[key]);QuickSort(arr, begin, left - 1);QuickSort(arr, left + 1, end);}
}

 还有10个数的时候我们调用其他排序,虽然但是效率提高了一点点,但是没有太多,所以我们了解一下就行。

6.2 挖坑法

接下来就是挖坑的介绍了,我们要先记住一个点就是,单趟快排的基本点就是让一个数到它正确的位置,那么怎么到正确的位置呢?即让左边的数小于它,右边的数大于它就行。基本思想是不变的。

挖坑的具体做法是这样的,将arr[keyi]存放到一个临时变量里面去,此时第一个坑位形成,即是arr[keyi],然后同样的,右边开始找小的,找到了,该数据给坑位,此时右边找到的位置就是一个坑位,左边同理,找到一个大的,给值,变坑位,最后左右相遇,就是keyi的正确位置了。

代码如下:

int PartSort2(int* arr, int begin, int end)
{//三数取中int midi = GetMidi(arr, begin, end);swap(arr[begin], arr[midi]);int key = arr[begin];int hole = begin;while (begin < end){//等于是必要的while (arr[end] >= key && begin < end){end--;}arr[hole] = arr[end];hole = end;while (arr[begin] <= key && begin < end){begin++;}arr[hole] = arr[begin];hole = begin;}arr[hole] = key;return hole;
}

挖坑相对hoare版本的要理解多,但是总体思想是不变的。

6.3 前后指针法

前后指针的单趟思想也是没有变的,还是让一个数到它的正确位置上,不过这个到的过程可能有点差异而已,前后指针利用的是两个指针,指针prev最开始指向数组首元素,指针cur指向的是首元素的下一个位置,整个过程是,cur先走,判断该位置是否小于arr[keyi],小于的话,prev就跟上来,如果是大于的话,那么prev就在原地不动,当cur指向的位置再次小于arr[keyi],并且prev和cur之间隔着几个大的元素的时候,那么++prev之后交换cur指向的那个位置,重复这个过程即可,因为prev指向的就是比arr[keyi]小的位置,cur指向的是大的位置,交换则是为了最后正确位置做铺垫,最后的情况就是,prev和cur间隔的所有元素都比arr[keyi]大,此时prev的位置就是正确位置,交换prev的位置和arr[keyi]的位置即可完成一次单趟。

代码如下:

int QuickS(int* arr, int begin, int end)
{//三数取中int midi = GetMidi(arr, begin, end);swap(arr[begin], arr[midi]);int keyi = begin, prev = begin, cur = prev + 1;while (cur <= end){if (arr[cur] < arr[keyi] && ++prev != cur)swap(arr[prev], arr[cur]);cur++;}swap(arr[keyi], arr[prev]);return prev;
}

第一步肯定是三数取中,然后是选定三个下标,分别是keyi,prev,cur,因为begin - end是左闭右闭区间,所以循环结束条件是cur <= end,判断条件这里可能看着有点复杂,第一个是判断是否小于arr[keyi],如果满足,那么++prev之后,不等于cur,就可以进行交换了,最后cur++,这里如果++prev != cur放在前面就不可以了,破坏了先决条件 arr[cur] < arr[keyi],所以顺序也是要注意的,最后就是交换,返回对应下标了。

6.4 非递归快排

前面使用递归快排的时候,不难发现递归的顺序是根,左子树,右子树,那么我们非递归快排的时候实现的也是这种顺序模式,但是我们不能用递归,就需要利用到其他数据结构了,这里呢用到的是栈,即我们将区间段存进去,排序好之后再返回一些区间段,反复该操作,那么我们该如果排序呢?前面已经封装好了单趟的排序,所以我们可以直接复用即可,这也是为什么要封装单趟排序。

对于栈的使用,我们只需要记住,不停进栈,出栈即可,所以整个排序模式下来就是:

进栈,出栈,排序,返回区间段,就可以了,代码如下:

void QuickNR(int* arr, int begin, int end)
{//栈不是用来存储数据的 存储的是区间stack<int> s;s.push(end);s.push(begin);while (!s.empty()){int left = s.top();s.pop();int right = s.top();s.pop();int keyi = QuickS(arr, left, right);if (left < keyi - 1){s.push(keyi - 1);s.push(left);}if (right > keyi + 1){s.push(keyi + 1);s.push(right);}}
}

当栈为空的时候,也就是没有区间可以排序了,即所有区间都有序了,此时排序就完成了,先压begin还是end都是可以的,无非就是循环的top的顺序需要改一下。

快排的时间复杂度是O(N * logN),空间复杂度为O(logN);


7 归并排序

7.1递归版本归并

在前面介绍树的时候,介绍到过一道题,求总共的节点个数,具体做法就是整体二叉树看作是一个一个小的二叉树,从最低层的二叉树慢慢的归并上来,归并排序的思想和树的思想是一样的,我们不是要排序吗?那么我们排一个数的序,排序好了再排两个数的序,然后再是4个数排序,然后是8个数的排序,直到排完即可。

所以归并排序这里也是要用到递归的,递归到一个数的时候,就可以返回开始排序了,排序的时候我们是这样排的,两个数组成的集合已经有序,那么我们就两两比较,谁小谁就尾插即可,难就是难在如何递归上面:

void Merges(int* arr,int begin, int end, int* tmp)
{if (begin >= end)return;//递归int mid = (begin + end) / 2;Merges(arr, begin, mid, tmp);Merges(arr, mid + 1, end, tmp);//归并int begin1 = begin, end1 = mid;int begin2 = mid + 1, end2 = end;int i = begin;while (begin1 <= end1 && begin2 <= end2){if (arr[begin1] <= arr[begin2]){tmp[i++] = arr[begin1++];}else{tmp[i++] = arr[begin2++];}}while (begin1 <= end1){tmp[i++] = arr[begin1++];}while (begin2 <= end2){tmp[i++] = arr[begin2++];}memcpy(arr + begin, tmp + begin, sizeof(int) * (end - begin + 1));
}
void MergeS(int* arr, int size)
{int* tmp = new int[size];Merges(arr,0,size - 1,tmp);delete[] tmp;
}

这里因为tmp的原因所以加了一个函数,当然也可以都写在一个函数里面,但是不太好控制,这里建议的就是分别写在两个函数。

归并的时间复杂度是O(N * logN),空间复杂度是O(N)。

7.2 非递归版本归并

同样,归并也有非递归版本,也就是左子树 右子树,根,递归那里,是从1 -> 2 -> 4 -> 8这样排序好的,也就是说我们非递归就是要直接实现这个过程,所以我们需要一个变量用来计算我们一次性要排序多少组,这里用到了gap,那么数组里面的每个gap组拍好了之后,gap就应该*2,重复这个过程就可以,那么越界的问题我们应该如何处理?

越界嘛,无非就是begin1 end1 begin2 end2出现了问题,那么如果是begin2 和 end1出现了问题直接break就可以了,因为已经排序完成,end2出现问题我们进行修正就可以了,begin1出问题?那基本不会的,所以,代码如下:

void MergeSortNonR(int* arr, int n)
{int* tmp = new int[sizeof(int) * n] {0};int gap = 1;while (gap < n){//整体排序 排序好了gap改变,模仿 1 -> 2 -> 4 -> 8for (size_t i = 0; i < n; i += 2 * gap){int begin1 = i, end1 = i + gap - 1;int begin2 = i + gap, end2 = i + 2 * gap - 1;//排序完成if (end1 >= n || begin2 >= n){break;}//后面部分差点意思if (end2 >= n){end2 = n - 1;}//开始归并int j = begin1;while (begin1 <= end1 && begin2 <= end2){if (arr[begin1] < arr[begin2]){tmp[j++] = arr[begin1++];}else{tmp[j++] = arr[begin2++];}}while (begin1 <= end1){tmp[j++] = arr[begin1++];}while (begin2 <= end2){tmp[j++] = arr[begin2++];}memcpy(arr + i, tmp + i, sizeof(int) * (end2 - i + 1));}gap *= 2;}delete[] tmp;
}

8 计数排序

计数排序是很特殊的,因为它不是比较排序,往往排序的思想都是,比较谁大谁小再决定怎么操作,它不一样,它是通过数组的下标来排序的,计数排序的具体操作是先开空间,空间开多大呢?空间大小是排序的数集合的最大值减去最小值,所以排序有一个操作就是要找最大值最小值,其次就是,为什么要开这么大的空间?我只要最大的不行吗?也可以,但是空间浪费是很大的,比如排序 11101 99999,就会浪费10000多个空间,所以这里用到了相对映射。

空间开好了之后,就是用下标,判断每个下标对应的数字有多少,此时最小值就用上了,++之后,判断count下标对应的数字有多少,就赋值就行了。如下:

void Countsort(int* arr, int size)
{int max = arr[0];int min = arr[0];//找最值for (int i = 0; i < size; i++){if (arr[max] < arr[i])arr[max] = arr[i];if (arr[min] > arr[i])arr[min] = arr[i];}int* count = new int[(max - min + 1) * sizeof(int)] {0};for (int i = 0; i < size; i++){count[arr[i] - min]++;}int j = 0;for (int i = 0; i < max - min + 1; i++){while (count[i]--){arr[j++] = i + min;}}delete[] count;
}

时间复杂度为O(N),空间复杂度为O(N),效率高得离谱。


9 排序总结

这里涉及稳定性的概念,稳定性是指相同的数,排序之后相对位置不变,所以这里直接给一张图了,自行体会了哦~


感谢阅读!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/873167.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Torch-Pruning 库入门级使用介绍

项目地址&#xff1a;https://github.com/VainF/Torch-Pruning Torch-Pruning 是一个专用于torch的模型剪枝库&#xff0c;其基于DepGraph 技术分析出模型layer中的依赖关系。DepGraph 与现有的修剪方法&#xff08;如 Magnitude Pruning 或 Taylor Pruning&#xff09;相结合…

TCP重传机制详解

1.什么是TCP重传机制 在 TCP 中&#xff0c;当发送端的数据到达接收主机时&#xff0c;接收端主机会返回⼀个确认应答消息&#xff0c;表示已收到消息。 但是如果传输的过程中&#xff0c;数据包丢失了&#xff0c;就会使⽤重传机制来解决。TCP的重传机制是为了保证数据传输的…

React安装(学习版)

1. 安装Node.js和npm 首先&#xff0c;确保你的电脑上已经安装了Node.js和npm&#xff08;Node Package Manager&#xff09;。你可以从 Node.js官网 下载安装包并按照提示进行安装。安装完成后&#xff0c;可以在命令行终端中验证Node.js和npm是否正确安装&#xff1a; node …

【Node.js】初识 Node.js

Node.js 概念 Node.js 是一个开源与跨平台的 JavaScript运行时环境 &#xff0c;在浏览器外运行 V8 JavaScript 引擎(Google Chrome的内核)&#xff0c;利用事件驱动、非阻塞和异步输入输出 等技术提高性能。 可以理解为 Node.js就是一个服务器端的、非阻塞式 l/O 的、事件驱…

01 MySQL

学习资料&#xff1a;B站视频-黑马程序员JavaWeb基础教程 文章目录 JavaWeb整体介绍 MySQL1、数据库相关概念2、MySQL3、SQL概述4、DDL:数据库操作5、DDL:表操作6、DML7、DQL8、约束9、数据库设计10、多表查询11、事务 JavaWeb整体介绍 JavaWeb Web&#xff1a;全球广域网&…

PostgreSQL的逻辑架构

一、PostgreSql的逻辑架构&#xff1a; 一个server可以有多个database&#xff1b;一个database有多个schema&#xff0c;默认的schema是public&#xff1b;schema下才是对象&#xff0c;其中对象包含&#xff1a;表、视图、触发器、索引等&#xff1b;与user之间的关系&#x…

Mysql笔记-20240718

零、 help、\h、? 调出帮助 mysql> \hFor information about MySQL products and services, visit:http://www.mysql.com/ For developer information, including the MySQL Reference Manual, visit:http://dev.mysql.com/ To buy MySQL Enterprise support, training, …

免费恢复软件有哪些?电脑免费使用的 5 大数据恢复软件

您是否在发现需要的文件时不小心删除了回收站中的文件&#xff1f;您一定对误操作感到后悔。文件永远消失了吗&#xff1f;还有机会找回它们吗&#xff1f;当然有&#xff01;您可以查看这篇文章&#xff0c;挑选 5 款功能强大的免费数据恢复软件&#xff0c;用于 Windows 和 M…

<数据集>混凝土缺陷检测数据集<目标检测>

数据集格式&#xff1a;VOCYOLO格式 图片数量&#xff1a;7353张 标注数量(xml文件个数)&#xff1a;7353 标注数量(txt文件个数)&#xff1a;7353 标注类别数&#xff1a;6 标注类别名称&#xff1a;[exposed reinforcement, rust stain, Crack, Spalling, Efflorescence…

又缩水Unity7月闪促限时4折活动模块化角色模板编辑器场景美术插件拖尾怪物3D模型UI载具AI对话TPS飞机RPG和FPS202407

Flash Deals are Coming Back! 限时抢购又回来了&#xff01; July 17, 2024 8:00:00 PT to July 24, 2024 7:59:00 PT 太平洋时间 2024 年 7 月 17 日 8&#xff1a;00&#xff1a;00 至 2024 年 7 月 24 日 7&#xff1a;59&#xff1a;00&#xff08;太平洋时间&#xff09;…

云计算实训室的核心功能有哪些?

在当今数字化转型浪潮中&#xff0c;云计算技术作为推动行业变革的关键力量&#xff0c;其重要性不言而喻。唯众&#xff0c;作为教育实训解决方案的领先者&#xff0c;深刻洞察到市场对云计算技能人才的迫切需求&#xff0c;精心打造了云计算实训室。这一实训平台不仅集成了先…

软件著作权申请教程(超详细)(2024新版)软著申请

目录 一、注册账号与实名登记 二、材料准备 三、申请步骤 1.办理身份 2.软件申请信息 3.软件开发信息 4.软件功能与特点 5.填报完成 一、注册账号与实名登记 首先我们需要在官网里面注册一个账号&#xff0c;并且完成实名认证&#xff0c;一般是注册【个人】的身份。中…

网安小贴士(17)认证技术原理应用

前言 认证技术原理及其应用是信息安全领域的重要组成部分&#xff0c;涉及多个方面&#xff0c;包括认证概念、认证依据、认证机制、认证类型以及具体的认证技术方法等。以下是对认证技术原理及应用的详细阐述&#xff1a; 一、认证概述 1. 认证概念 认证是一个实体向另一个实…

llama 2 改进之 RMSNorm

RMSNorm 论文&#xff1a;https://openreview.net/pdf?idSygkZ3MTJE Github&#xff1a;https://github.com/bzhangGo/rmsnorm?tabreadme-ov-file 论文假设LayerNorm中的重新居中不变性是可有可无的&#xff0c;并提出了均方根层归一化(RMSNorm)。RMSNorm根据均方根(RMS)将…

解决npm install(‘proxy‘ config is set properly. See: ‘npm help config‘)失败问题

摘要 重装电脑系统后&#xff0c;使用npm install初始化项目依赖失败了&#xff0c;错误提示&#xff1a;‘proxy’ config is set properly…&#xff0c;具体的错误提示如下图所示&#xff1a; 解决方案 经过报错信息查询解决办法&#xff0c;最终找到了两个比较好的方案&a…

HTTP协议、Wireshark抓包工具、json解析、天气爬虫

HTTP超文本传输协议 HTTP&#xff08;Hyper Text Transfer Protocol&#xff09;&#xff1a; 全称超文本传输协议&#xff0c;是用于从万维网&#xff08;WWW:World Wide Web &#xff09;服务器传输超文本到本地浏览器的传送协议。 HTTP 协议的重要特点&#xff1a; 一发一收…

JVM:MAT内存泄漏检测原理

文章目录 一、介绍 一、介绍 MAT提供了称为支配树&#xff08;Dominator Tree&#xff09;的对象图。支配树展示的是对象实例间的支配关系。在对象引用图中&#xff0c;所有指向对象B的路径都经过对象A&#xff0c;则认为对象A支配对象B。 支配树中对象本身占用的空间称之为…

Spire.PDF for .NET【文档操作】演示:如何在 C# 中切换 PDF 层的可见性

我们已经演示了如何使用 Spire.PDF在 C# 中向 PDF 文件添加多个图层以及在 PDF 中删除图层。我们还可以在 Spire.PDF 的帮助下在创建新页面图层时切换 PDF 图层的可见性。在本节中&#xff0c;我们将演示如何在 C# 中切换新 PDF 文档中图层的可见性。 Spire.PDF for .NET 是一…

【LabVIEW作业篇 - 1】:中途停止for和while循环

文章目录 for循环while循环如何使用帮助 for循环 在程序框图中&#xff0c;创建for循环结构&#xff0c;选择for循环&#xff0c;鼠标右键-条件接线端&#xff0c;即出现像while循环中的小红圆心&#xff0c;其作用与while循环相同。 运行结果如下。&#xff08;若随机数>…

分布式搜索引擎ES-Elasticsearch进阶

1.head与postman基于索引的操作 引入概念&#xff1a; 集群健康&#xff1a; green 所有的主分片和副本分片都正常运行。你的集群是100%可用 yellow 所有的主分片都正常运行&#xff0c;但不是所有的副本分片都正常运行。 red 有主分片没能正常运行。 查询es集群健康状态&…