您的位置:首页 > 理论基础 > 数据结构算法

C算法_基础数据结构18/1/30 8大排序

2018-02-04 17:24 465 查看

前情提要

在数据结构的学习中,必不可少的基本算法就是排序。排序算法可以分为内部排序和外部排序

内部排序是数据记录在内存中进行排序

外部排序是因排序的数据很大,一次不能容纳全部的排序记录,在排序过程中需要访问外存

常见的内部排序算法有:插入排序、希尔排序、选择排序、冒泡排序、归并排序、快速排序、堆排序、基数排序等

在基础数据结构中有8种必看的排序方法,这8中方法各有各的长处,也有其短板。每种排序都有自己的使用环境。特别是快排及堆排在实际处理问题时实用性很强。下面我们来看一下这8中排序:

插入排序

1,直接插入排序

2,shell排序

选择排序

1,直接选择排序

2,堆排序

归并排序

基数排序

在开始之前,有两点需要注意:

1,在叙述者8大排序时,我默认的排序模式为从小到大的顺序

2,默认整数排序,所以会采用数组

插入排序

一. 直接插入排序

直接插入排序就我自己学习基础数据结构而言是最简单的种排序之一,仅次于冒泡排序。但它是shell排序的基础实现。所以也有其学习的必要。

时间复杂度 :



问题 : 时间复杂度分了三种,为什么?

空间复杂度:O(1)

稳定性:稳定

在给出了这些基本定义之后,我们来看一下直接插入排序的实现思想:

其实直接插入排序的思想跟平常打扑克牌是一个道理,比如你跟别人斗地主,你摸到了这样4张牌:7,3,8,5 你在摸到第一张 7 的时候,就直接拿在手上,第二张 3 比 7 小,一般都会放在 7 的前面,第三张 8 比 7 大一般放在 7 的后面,如此排序,那你手上的牌最后会是 3,5,7,8 。

那反观我们的直接插入排序,也是这样的思想,比如有一个数组arr[4] = {7,3,8,5}; 那我们可以模仿摸牌的动作,选取一个数字作为我当前摸到的牌,在选取它旁边的一个数字作为我当前手上的牌。这里我们假设摸到的是 7 ,手上的牌是 3 ,7 比 3 大,我们应该将其放到 3 的后面。那此时数组就变为了arr[4] = {3,7,8,5}; 前两个数就有序了,以此类推,第二摸到的牌是 7 ,手上的牌是 8 ,7 比 8 小,不用移动。第三次摸到的牌是 8 ,手上的牌是 5 ,8 比 5 大,将其放到 5 的后面。那这次移动之后数组就变为了arr[4] = {3,7,5,8}; 这时我们再将刚才的过程走一遍就能得到排序好的数组。(即进行第二次遍历数组)

所以说直接插入排序的时间复杂度在数组无序时为O(n2),因为刚才的过程遍历了一次数组 是一个for循环,而且这只是一次排序,还需要一个for循环进行后续的排序。

代码实现

void Insert_arr(int *arr,int len)
{
assert(arr != NULL);
int i;//需要排序的次数
int j;
int tmp;//存放需要比较的元素(即当前手上的牌)
for (i = 1; i < len; i++)
{
tmp = arr[i];
for (j = i - 1; j >= 0; j--)
{
if (tmp < arr[j])//数据比arr[j]小即交换(arr[j]即为当前摸的牌)
{
arr[j + 1] = arr[j];//比当前手上的牌小即交换
}
else
{
break;
}
}
arr[j + 1] = tmp;
}
}


在代码中我们可以看到,我们定义了一个 tmp 来存放数组的第2个元素(即sort[ j ]此时 j 的值为0,i 为 1)的值,并与前一个元素(即数组第一个元素)进行比较,如果比前一个小则进行交换,然后 j 自减 1 ,不满足循环判断条件跳出。在这个过程中只进行了一次交换。然后进入外部循环,i 自加 1 ,进行下一次排序。

上面的说的东西可能很晦涩,是因为例子举得不够特殊,我们可以看一个比较特殊的例子:

arr[ 4 ] = { 3,2,1,0 };在这个例子中,如果按照直接插入排序的话,基本是达到最大的时间复杂度了,因为基本每次进入内部循环时都需要交换。

我们给出如下的流程:



图中给出了前两次循环,后面的方法一致。

通过上面的描述我们便可以回答刚才的问题了,可以看出在直接插入排序中,越有序的数据进入内部循环的次数越少,时间复杂度越低,在数据本来有序的情况下便可以完全不进入内循环,所以时间复杂度就为O(n)ps:当然有序的数也不用我们排序,这里只是考虑极端情况!!

二. shell排序

shell排序其实就是直接插入排序的一种优化版本,在直插法的基础上的一种降低时间复杂度的算法。

时间复杂度:



问题:前面说shell是为了降低时间复杂度,但这里只降低了平均值,为什么?

空间复杂度:O(1)

稳定性:不稳定

同样的看一下shell排序的实现思想:

采用分组的思想,把一组数字分为若干的小组,而在分组的过程当中,并不是几个紧挨着的分组,而是采用特定的分组方式,每一次都让组内有序,这样排序的好处是,每次排序都是将小的数据尽量往前赶,大的数据尽量往后赶。在这里面,每一组的数字个数,在每一次分组的时候,都会缩小增量,比如第一次分组每组 3 个,第二次每组 5 个,第三次每组 1 个这样去分。(最后一次就直接进行一次插入排序)

优点在于,如果不执行前面的分组过程的话,数据的移动次数更多,更复杂,经过排序之后,数据已经越来越有序了。( 利用直接插入排序特性,前面提到过,越有序越高效 )

代码实现

void Shell(int *arr, int gap, int len)
{
assert(arr != NULL);
int i;
int j;
int tmp;
for (i = gap; i < len; i++)//不能用 += gap    并没有起到提高效率的作用
{
tmp = arr[i];
for (j = i - gap; j >= 0; j -= gap)
{
if (arr[j] > tmp)
{
arr[j + gap] = arr[j];
}
else
{
break;
}
}
arr[j + gap] = tmp;
}
}
void Shell_arr(int *arr,int len)
{
assert(arr != NULL);
int group[] = { 5,3,1};//分组必须互为素数
int len_group = sizeof(group) / sizeof(group[0]);
int i;
for (i = 0; i < len_group; i++)
{
Shell(arr, group[i],len);//传入分组信息
}
}


在代码中我们可以看到第一个函数跟上面的直插法排序代码一模一样,只是加入了一个gap,如果不清楚 gap 是啥,不用着急,我们看它来自哪里,他是第二个函数中制作好的数组中的元素,那我们已经看完了直插法排序,所以直接跳过第一个函数,只需要知道 gap 的制作过程即可

在第二个函数中,我们定义了一个数组group[] = {5,3,1};那么这个就是我们对直插法的优化分组方案,这里用文字解释很晦涩,我们来看一组解释图又可以明了了。

这里给出分组【规定】:分组元素必须互为素数;例如{3,5,1};

下面看看流程:



我们来考虑刚才的问题,其实我们可以看出,shell排序只是一种比较简单粗暴的优化,你直插排序不是越有序越牛逼吗?那我就先让你有点序。而且对于刚才的分组问题,在严蔚敏的《数据结构》一书中有过论述:(以下摘自《数据结构》)

希尔排序的分析是一个复杂的问题,因为它的时间是所取“增量”序列的函数,这涉及一些数学上尚未解决的难题。因此,到目前为止尚未有人求得一种最好的增量序列,但大量的研究已得出一些局部结论。但一般认为其时间复杂度在O(n^1.3) — O(n^1.5)之间。

选择排序

一:直接选择排序

选择排序跟我们熟知的冒泡排序很相似,但这种排序是不稳定的。

时间复杂度:



空间复杂度:O(1)

稳定性:不稳定

先看一下直接选择排序的实现思想:

直接选择排序是一种比较粗暴的方法,首先它有内外两层循环,在每次的内部循环中都会选定一个数与它后面的每一个数进行比较,将比它小的与它进行交换,这样就保证了该数是后面所有数中最小的,即包括该数在内的前面的所有数都是从小到大排列的,在进入第二次循环时,就选择前一次选择的下一个数,如此循环便可排列。

代码实现

void Select_Sort1(int *arr, int len)
{
int tmp;
int i;
int minx_index;//定义插入位置
for (i = 0; i < len - 1; i++)
{
minx_index = i;
for (int j = i + 1; j < len; j++)
{
if (arr[j] < arr[minx_index])
{
tmp = arr[j];
arr[j] = arr[minx_index];
arr[minx_index] = tmp;
}
}
}
}


代码很清晰也就不解释了,我们来看一下其流程:



二:堆排序

堆排序在八大排序中的地位与快速排序并列第一,不论其实用性还是其应用的广泛都是很值得一学的算法。但其本身也不太好理解。我们来看一看。

时间复杂度:



问题:为啥会有nlog2n这么奇葩的时间复杂度?

空间复杂度:O(1)

稳定性:不稳定

先来了解一下其实现的思想:

堆排序的实现借助于二叉树的思路,其本身是一棵顺序存储的完全二叉树,其中每个结点的关键字都不小于其孩子结点的关键字。这样说可能不太好理解,不用担心,这个本身就补能用文字去叙述。得上图:

在了解堆排之前还有一个知识的储备:大根堆与小根堆,每个分叉下的数字为它的节点的孩子节点:



在这里,因为我们是要把数字从小到大排列,所以要选用大根堆(为啥是大根堆?)的方法。

实现思路

由于文字不好描述,直接上图,看了之后在进行分析:



我们现在可以回答刚才的问题了,为啥要用大根堆?从流程可以看出来,每一次的排序中,除了上一次已找到的最大值之外,都能找到二叉树中的最大值,这就是大根堆的作用,然后我们进行交换,将最大值换到最后,如此往复便可成功排序。

但看了这个流程,我们会发现有一个很难以解决的问题,就是我每一次找到最后一个树杈之后,如何通过当前的父节点推出下一个我要排序的父节点呢?

对于上面这个问题我们可以考虑到有这样两种情况:

1,父推子:在父节点与子节点进行比较的时候,我们需要通过父节点的下标推出其子节点的下标。

2,子推父:在一颗树杈比较完了之后需要找到当前父节点的父节点进行比较,即寻找下一棵树杈。

我们给出下列示意图:



父推子 2n + 1(左) 2n + 2(右);

子推父 (n - 1) / 2

这样一来我们便有了整个堆排的思路及关键问题的解决方案了,下面贴出代码:

/*  非递归实现堆排序
*  堆排
*  父推子        n      2n + 1   2n + 2;
*  子推父        n      (n - 1) / 2
*/
void Adjust(int *arr, int start, int end)
{
assert(arr != NULL);
int tmp;
tmp = arr[start];
for (int i = 2 * start + 1; i <= end; i = 2 * i + 1)
{
if (i < end && arr[i] < arr[i + 1])
{
i++;//最大值下标
}
if (arr[i] > tmp)
{
arr[start] = arr[i];//大者为父
start = i;//继续遍历下一棵树
}
else
{
break;
}
}
arr[start] = tmp;
}

void Heap_Sort(int *arr, int len)
{
int be;
int tmp;
for (be = (len - 2) / 2; be >= 0; be--)
{
Adjust(arr, be, len - 1);
}

for (int i = 0; i < len - 1; i++)//交换最大值
{
tmp = arr[0];
arr[0] = arr[len - 1 - i];
arr[len - 1 - i] = tmp;
Adjust(arr, 0, len - 1 - i - 1);
}
}


也给出递归版本,但代码有点粗糙,只提供一个思路:

void Heap_Find_Max(int *arr, int len, int i)
{
assert(arr != NULL);

int Lchild = i * 2 + 1;//定义左儿子
int Rchild = i * 2 + 2;//定义右儿子
int Farther = i;//定义父亲
int tmp = Lchild;//定义中间变量,赋初值右儿子

if (Lchild < len)//递归失衡条件
{    //右儿子小于len则没有右儿子,tmp依然等于左儿子
if (Rchild < len && arr[Lchild] < arr[Rchild])//如果右小于左则替换
{
tmp = Rchild;
}
if (arr[tmp] > arr[Farther])//大者为父
{
Swap(arr, tmp, Farther);
Farther = tmp;//更换父亲
//父子递归
Heap_Find_Max(arr, len, Farther);//传入父亲
}
}
}

//制造大根堆
void Make_Heap(int *arr, int len)
{
assert(arr != NULL);
int i;
//该处可以用(len / 2 - 1)
for (i = (len - 2) / 2; i >= 0; i--)//制造父亲
{
Heap_Find_Max(arr, len, i);
}
}
void Heap_sort(int *arr, int len)
{
int i;
int temp;
Make_Heap(arr, len);
for (i = len - 1; i >= 0; i--)//交换最大值
{
temp = arr[0];
arr[0] = arr[i];
arr[i] = temp;

Heap_Find_Max(arr, i, 0);
}
}


到这里堆排序这个大块算是完了,前面也提到了堆排的重要性,其在实际应用很多。所以很有学习的必要性。

交换排序

一,冒泡排序

相信同学们在大学里面学但凡学过一点C语言的,都接触过冒泡排序吧!这是一个非常基础的排序方法,也经常拿做C语言基础教学的一个算法习题。

还是先看一下时间复杂度:



空间复杂度:O(1)

稳定性:稳定

关于冒泡的实现思想就不用我多提了,直接来看一下代码吧:

void Bubble_Sort(int *arr,int len)
{
int i;
int j;
int tmp;
bool swap = false;
for(i = 0;i < len;i++)//1 2 3 4 5 6 7
{
swap = false;
for(j = 0;j < len-1-i;j++)
{
if(arr[j] > arr[j+1])
{
tmp = arr[j];
arr[j] = arr[j+1];
arr[j+1] = tmp;
swap = true;
}
}
if(!swap)
{
return;
}
}
}


冒泡排序的内容没有多少可说的,我们也都很清楚。这里就不继续了。

二,快速排序

前面多次提到了快排,而且也提到过其重要性。快排之所以叫快排实在是因为其排序速度太快了,在数量级较小的数列中进行排序并不能体会其差别,但一旦到了以千万或者亿的级别其速度的优势就很明显了,在博客后面会对8大排序的速度进行一个比较,就可以体会到各种排序的差别了。

给出时间复杂度:



空间复杂度:O(nlog2n)

稳定性:不稳定

对于这个时间复杂度的问题,这里不做过多解释,给出一个博客,里面有对这个奇葩的时间复杂度的探究:

https://www.cnblogs.com/fengty90/p/3768827.html

先看一下其实现的思路:

快排的实现思路从大体来讲其实就是一个找基准的过程,找出一个元素(理论上可以随便找一个)作为基准(pivot),然后对数组进行分区操作,使基准左边元素的值都不大于基准值,基准右边的元素值 都不小于基准值,如此作为基准的元素调整到排序后的正确位置,是不是又有点饶了,不打紧,先看代码后面会对代码作出解释:

代码实现

//快速排序一次排序(实现代码)
int Partion(int *arr, int low, int high)
{
assert(arr != NULL);
int tmp;//定义中间变量
tmp = arr[low];//将需要比较的数字存放在tmp中
while (low < high)
{
while ((low < high) && arr[high] >= tmp)
{
high--;//没找到比中间变量小的继续遍历
}
if (low >= high)
{
break;
}
else
{
arr[low] = arr[high];
}
while ((low < high) && arr[low] <= tmp)
{
low++;
}
if (low >= high)
{
break;
}
else
{
arr[high] = arr[low];//没找到比中间变量大的继续遍历
}
}
arr[low] = tmp;//放回基准值
return low;//返回下一次基准
}
//快速排序(递归)
void Quick(int *arr, int head, int tail)
{
int mid_numb = Partion(arr, head, tail);//获得基准
if (mid_numb < tail - 1)
{
Quick(arr, mid_numb + 1, tail);//传入右基准
}
if (mid_numb > head + 1)
{
Quick(arr, head, mid_numb - 1);//传入左基准
}
}
void Quick_Sort(int *arr, int len)//辅助递归调用
{
Quick(arr, 0, len - 1);
}


在看完代码之后可能更懵了,别急我们对着代码来看它的流程:





相信这一套流程打下来,这个一次快排已经可以秒懂了,下来的步骤只是重复这个流程,直到排序好为止。

刚才的代码是递归实现,下面也给出非递归的实现,其实现思路是利用模拟栈存储下一次基准并在下一次排序时出栈释放基准。

给出非递归代码:

//快速排序(非递归 ,栈模拟方法)
void Quick_Sort2(int *arr, int len)
{
assert(arr != NULL);
int len_st = (int)(log10((double)len) / log10((double)2));
int *stack = (int*)malloc(sizeof(int)* 2 * len_st);
assert(stack != NULL);

int top = 0;
int i = 0;
int j = len - 1;
int mid_numb = Partion(arr, i, j);

if (mid_numb < j - 1)
{
stack[top++] = mid_numb + 1;
stack[top++] = j;
}
if (mid_numb > i + 1)
{
stack[top++] = i;
stack[top++] = mid_numb - 1;
}
while (top >0)//模拟栈不为空则开始出栈
{
j = stack[--top];
i = stack[--top];
mid_numb = Partion(arr, i, j);
if (mid_numb < j - 1)
{
stack[top++] = mid_numb + 1;
stack[top++] = j;
}
if (mid_numb > i + 1)
{
stack[top++] = i;
stack[top++] = mid_numb - 1;
}
}
free(stack);//释放动态模拟栈空间
stack = NULL;
}


这里只给出代码,就不做过多解释了。其实快排还有很多种优化方案,这里也给出其中一种叫做“基准聚拢法”的优化方案,其基本思想就是现将数列中一样的数字聚集在一起,这样就可以省去很多步骤。

仅供参考:

//********基准聚拢法********
void Focus_Numbs(int *arr, int par, int *left, int *right, int start, int end)
{
assert(arr != NULL);
int i;
int Left_par = par - 1;//聚拢左边相同数
for (i = par - 1; i >= start; i--)
{
if (arr[par] == arr[i])
{
if (i != Left_par)
{
Swap(arr, i, Left_par);
Left_par--;
}
else
{
Left_par--;
}
}
}
int Right_par = par + 1;//聚拢右边相同数
for (i = par + 1; i <= end; i++)
{
if (arr[par] == arr[i])
{
if (i != Right_par)
{
Swap(arr, i, Right_par);
Right_par++;
}
else
{
Right_par++;
}
}
}
*right = Right_par;
*left = Left_par;
}


在给出随机选取基准法:

void Swap(int *arr,int low,int high)
{
int tmp = arr[low];
arr[low] = arr[high];
arr[high] = tmp;
}
void Quick(int *arr,int start,int end)
{
Swap(arr,start, rand()%(end-start) + start);//每次需要在high这面产生随机值
int par = Partion(arr,start,end);
if(par > start+1)//左边是不是有两个数据以上
{
Quick(arr,start,par-1);
}
if(par < end-1)
{
Quick(arr,par+1,end);
}
}


快排也没有什么需要说明的了,这是一个大块·················

归并排序

归并排序在8大排序中独占一块,其重要性也是不可小觑的,其基本思想就是将大块数据分为小块,小块在分小,然后开始排序,其在实际中也会有应用,例如在面对很庞大的数量级的数列进行排序时,可以将数据分别放入多个文件中,先让各个文件内数据有序,在进行总的一次排序。

时间复杂度:



空间复杂度:O(n)

稳定性:稳定

堆排序的思路上面已经差不多表述完了,其实就是“大事化小,小事在化小”,将数列分为很多小组,先让组内有序,在合并排序。

还是先给出代码:

//归并排序
void Mearge(int *arr, int len, int group)
{
assert(arr != NULL);
int *mid_arr = (int *)malloc(sizeof(int)* len);//这里可以解释空间复杂度为O(n)
assert(mid_arr != NULL);

int i = 0;
int head_one = 0;//一号队头
int tail_one = head_one + group - 1;//一号队尾
int head_two = tail_one + 1;//二号队头
int tail_two = head_two + group - 1 < len - 1 ? head_two + group - 1 : len - 1;//二号队尾,考虑奇数数组元素情况
while (head_two < len)
{
while (head_one <= tail_one && head_two <= tail_two)
{
if (arr[head_one] < arr[head_two])//较小元素入栈
{
mid_arr[i++] = arr[head_one++];
}
else
{
mid_arr[i++] = arr[head_two++];
}
}
//判断每个队中有无剩余元素
while (head_one <= tail_one)
{
mid_arr[i++] = arr[head_one++];
}
while (head_two <= tail_two)
{
mid_arr[i++] = arr[head_two++];
}
//更新队头 队尾
head_one = tail_two + 1;
tail_one = head_one + group - 1;
head_two = tail_one + 1;
tail_two = head_two + group - 1 < len - 1 ? head_two + group - 1 : len - 1;
}
//最后判断一号队头后有无剩余元素
while (head_one < len)
{
mid_arr[i++] = arr[head_one++];
}
//放回排序好数组元素
for (i = 0; i < len; i++)
{
arr[i] = mid_arr[i];
}

free(mid_arr);
mid_arr = NULL;
}
//分组函数
void Mearge_Sort(int *arr, int len)
{
for (int i = 1; i < len; i *= 2)//给定group 的元素分组
{
Mearge(arr, len, i);
}
}


代码可能也有点晦涩,我们还是看流程来解释吧:



可能我画的比较印象派,但归并也是我觉得最不好解释的一个,图化成这样就凑或着看吧,我尽力了。如果还有不明白的,可以说一个我自己遇见不会的代码的方法:

1,打开VS。2,贴上我的或别人的代码。3,打断点进行调试,打开监视窗口仔细观察数组内元素的走向。你就对这个代码的思路更加了解了。

基数排序

基数排序?·······这个方法在我看来有点奇怪,江湖人称“桶子法排序”,就是按规律将一队数字丢入一个桶内,而且反复丢,最后在出桶就排序完了。·~我第一次看到这个就更变魔术一样。有点儿神奇。

时间复杂度:



空间复杂度:O(rd+n)

稳定性:稳定

又称“桶子法”排序,他是根据待排序的每一位上的数字进行入“桶”排序,桶的数量跟当前单个数字的取值范围有关。当前数字是十进制,单个数字就是 0-9,类似队列。

如果拿数组实现:那么空间复杂度是 O(r*n)(r 桶的数量),n是数据的个数。如果拿单链表实现,那么每一个桶内的数据,每个数据是一个结点,每个桶都有一个头结点。相对来说更为简单空间复杂度小。

代码实现

//定义链表结构体
typedef struct Node
{
int data;
struct Node *next;
}Node,*List;
//初始化链表
void Insert_List(List plist)
{
assert(plist != NULL);
plist->next = NULL;
}
//获得新节点
static Node *GetNode(int val)
{
Node *p = (Node*)malloc(sizeof(Node));
assert(p != NULL);
p->data = val;
p->next = NULL;
return p;
}
//这里用尾插法
bool Insert_tail(List plist, int val)
{
assert(plist != NULL);
Node *pGet = GetNode(val);
Node *p = plist;
for (; p->next != NULL; p = p->next)
{
;
}
p->next = pGet;
return true;
}
//取出链表第一个节点数据
bool Pop_sort(List plist,int *rtv)
{
assert(plist != NULL);
Node *p = plist->next;
if (p == NULL)
{
return false;
}
if (rtv != NULL)
{
*rtv = p->data;
}
plist->next = p->next;
free(p);
p = NULL;
return true;
}
//获得数列最大数的位数
int GetMaxBit(int *arr, int len)
{
assert(arr != NULL);
int count = 0;
int tmp = 0;
for (int i = 1; i < len; i++)
{
if (arr[i] > arr[tmp])
{
tmp = i;
}
}
int numb = arr[tmp];
for (; numb != 0;)
{
count++;
numb /= 10;
}
return count;
}
//得到numb的第i位数字
int GetNum(int numb, int i)
{
int j;
for (j = 0; j < i; j++)
{
numb /= 10;
}
return numb % 10;
}
//桶排序主代码
void Radix(int *arr, int len, int i)
{
assert(arr != NULL);
Node head[10];
for (int j = 0; j < 10; j++)
{
Insert_List(&head[j]);
}
int tmp;
for (int j = 0; j < len; j++)
{
tmp = GetNum(arr[j], i);
Insert_tail(&head[tmp], arr[j]);
}
//出桶
int j = 0;
int n = 0;
for (; j < 10;)
{
if (Pop_sort(&head[j], &arr
))
{
n++;
}
else
{
j++;
}
}
}
void RadixSort(int *arr, int len)
{
//需要得到当前数据内最大值的位数 -- 进桶出桶次数
int count = GetMaxBit(arr, len);
for (int i = 0; i < count; i++)
{
Radix(arr, len, i);//i表示从右数第0位开始
}
}


代码在上,我就不解释了(是在是太长了)。

这里还是给出流程,看图吧!!





是不是跟我开始一样觉得有点神奇,莫名其秒就排好序了,但都是别人家的“脑袋”想出来的,如果不理解其中奥妙,不打紧,只需要知道其用法就可以了。

总结

那最后一个桶排序也就到这里了,到此为止8大排序就完了,当然这些全是我自己的理解,如果有解释不当的很正常,这是一篇适合初学者看的东西。并不是特别深,只对排序的实现思路和代码思路做了一个初步的解释。所以高手笑笑就好。~~~~
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息