系列文章目录
前言
排序对于程序员对数据结构的理解和运用都有很大的提升,我们今天所学在以后的工作事件中可以有着很多的应用,例如游戏中一键整理背包物品,网页中搜索内容的排序啊,这些都是排序的应用,也许他们的逻辑更加复杂,但底层的逻辑仍然是初阶排序
一、排序的类型有哪些
排序的方法有种种,我们只介绍最为基础的几种
1.冒泡排序
2.选择排序
3.插入排序
4希尔排序
5.归并排序
6.堆排序
7.快速排序
二、具体如何实现
1.冒泡排序
1.名词解释
所谓冒泡排序,顾名思义,就是像冒泡泡一样,一轮排序选出一个目标数字,可以是大也可以是小,看目标是升序还是降序。

2.代码实现
void BubbleSort(int* a, int n)
{
for(int i = 0;i<n-1;i++)
{
for(int j = 0;j<n-1-i;j++)
{
if(a[j]>a[j+1])
{
swap(&a[j],&a[j+1]);
}
}
}
}
解释:升序为例,我们要模拟冒泡泡的过程,就是需要相邻两个数的连续比较,这样才能让较大数字"浮起来",也就是到数列的最末尾,而又需要将n个数据拍成有序,就需要n-1次循环实现(最后一个数自然有序),所以代码中选择了两层循环嵌套,外层控制排序的轮次,内层控制一轮比较的逻辑(前一个数比后一个数大就交换)
3.复杂度分析
由于双层循环的嵌套,并且内层和外层都要经历大概n次循环,所以其时间复杂度为O(n^2),在数据较多时性能并不优秀
2.选择排序
1.名词解释
何为选择,以打牌为例,这种情况相当于我们在摸牌时不看,先接到手里,最后一把拿起来看,(以升序为例)然后每次选择最大的放在右手边,然后依次理顺排序

2.代码实现
void SelectSort(int* a, int n)
{
for(int i = 0;i<n/2;i++)
{
int mini = 0+i;
int maxi = n-1-i;
for(int j = i+1;j<n-i-1;j++)
{
if(a[j]<a[mini])
{
mini = j;
}
if(a[j]>a[maxi])
{
maxi = j;
}
}
swap(&a[i],&a[mini]);
swap(&a[n-1-i],&a[maxi]);
}
}
解释:为了尽可能减少复杂度,我们一轮循环选出最大和最小两个数,分别放于数列末尾和开头,实现逻辑非常简单不再赘述
3.复杂度分析
由于我们一次循环选择两个数排序,计算复杂度大概在n*n/2,也属于O(n^2)数量级
3.插入排序
1.名词解释
仍然以打牌为例,插入排序就像是我们常规的摸牌,摸一张牌,就把排放在其对应的位置上,插入排序的逻辑也是如此,每一次将新的元素放在已有的有序数组中从而完成排序

2.代码实现
void InsertSort(int* a, int n)
{
for(int i = 1;i<n;i++)
{
int tmp = a[i];
int j = i;
while(j>0&&a[j-1]>tmp)
{
a[j] = a[j-1];
j--;
}
a[j] = tmp;
}
}
解释:我们仍然使用两个循环嵌套的模式来模拟插入排序的过程,外循环中 i 表示要插入的元素的下标,因为一个数自然有序,所以 i 从1开始,内循环控制比较的次数,确定要插入的新元素的位置
3.复杂度分析
插入排序,复杂度也在O(n^2)数量级上
4.希尔排序
1.名词解释
希尔排序时插入排序的升级版本,再插入排序中我们只能对相邻元素的大小进行比较,这就导致了在排序后期如果出现比较小的数字,就需要经过多次交换才能够到达对应的位置,希尔排序通过使用一个gap变量,很好的将数据跳跃的距离放大了,能够将大小相差不大的数据放在一起,减少了交换的次数

2.代码实现
void ShellSort(int* a, int n)
{
int gap = n/3;
while(gap>0)
{
for(int i = gap;i<n;i+=gap)
{
int tmp = a[i];
int j = i;
while(j>0&&a[j-gap]>tmp)
{
swap(&a[j],&a[j-1]);
j-=gap;
}
a[j] = tmp;
}
gap/=2;
}
}
解释:希尔排序和插入排序关系紧密,多出了gap变量来调节步长,让数组先进性粗略的排序,将大小相差不大的放在一起,随着gap减小,数组变得更加有序,每一个循环的过程和插入排序类似,不多赘述。
3.复杂度分析
希尔排序平均复杂度约为O(n^1.3)
5.归并排序
1.名词解释
归并分为两步,递归和合并,通过递归每次将数组二分,通过合并将两侧已经有序的数组合并

2.代码实现
void MergeSort(int* a, int left,int right)
{
if(left >= right)
return;
int mid = (left+right)>>1;
MergeSort(a,left,mid);
MergeSort(a,mid+1,right);
int tmp[right-left+1];
int i = 0;
lefti = left;
righti = mid+1;
while(lefti<=mid&&righti<=right)
{
if(a[lefti]<=a[righti])
{
tmp[i++] = a[lefti++];
}
if(a[lefti]>a[righti])
{
tmp[i++] = a[right++];
}
}
while(lefti<=mid)
{
tmp[i++] = a[lefti++];
}
while(righti<=right)
{
tmp[i++] = a[righti++];
}
for(int i = 0;i<left-right+1;i++)
{
a[left+i] = tmp[i];
}
}
解释:前一部分没什么好说的,首先是递归返回条件,然后是递归排左半侧,递归排右半侧,在计算mid时使用了位操作数,左移一位意味着得到的数字除以二,更加快速,合并的过程先创建一个临时数组,比较已经有序的两个数组的头元素那个更小,就先放入那个元素,直至将两个数组中的元素都遍历完成
3.复杂度分析
递归每次除以2 的机制使得复杂度为对数级,最终复杂度为O(n*logn)
6.堆排序
1.名词解释
默认大家已经对堆这种数据结构有一定的了解,不清楚的可以先行学习一下再来,需要清楚父亲节点和孩子节点下标的关系,向上调整和向下调整等知识
2.代码实现
void AdjustDwon(int* a, int n, int root)
{
int parent = root;
int child = root*2+1;
while(child<n-1)
{
if(a[child]>a[child+1])
{
child++;
}
if(a[parent]>a[child])
{
swap(&a[parent],&a[child]);
}
int tmp = parent;
parent = child;
child = tmp*2+1;
}
if(n==2)
{
if(a[0]>a[1])
swap(&a[0],&a[1]);
}
}
void HeapSort(int* a, int n)
{
for(int i = n/2-1 ;i>=0;i--)
{
AdjustDwon(a,n,i);
}
for(int i = 0;i<n-1;i++)
{
swap(&a[0],&a[n-1-i]);
AdjustDwon(a,n-1-i,0);
}
}
解释:先使用向下调整建堆,这里构建大根堆,进行升序排序,向下调整建堆的关键是从倒数第一个非叶子节点开始,每多一个元素向下调整一次
3.复杂度分析
由于堆的数据结构特点,堆排序的复杂度也是O(n*logn)
7.快速排序
1.名词解释
好的那么我们进入本节的重难点,快速排序,听名字就知道这个排序强的可怕,重要程度也是不言而喻。一句话讲完快速排序的核心思想,就是找到 "轴" 的位置。每次选择一个元素为轴元素,比轴元素大的放一边,比轴元素小的放在另外一边,每次确定好一个轴的位置,直至数组有序
2.代码实现
快速排序的实现代码有多种,一 一为大家讲解,为了统一下面代码都排升序
(1)hoare版本
int PartSort1(int* a, int left, int right)
{
int axle = left;
while(left<right)
{
if(a[right]>=a[axle])
{
right--;
}
else if(a[left]<=a[axle])
{
left++;
}
else
{
swap(&a[left],&a[right]);
}
}
swap(&a[axle],&a[right]);
return right;
}
void QuickSort(int* a, int left, int right)
{
if(left >= right)
return;
int mid = PartSort1(a,left,right);
QuickSort(a,left,mid-1);
QuickSort(a,mid+1,right);
}
解释 :这个版本的快排思路如下,先从右侧开始找,右侧找到比轴值小的元素找到为止,然后左侧开始向右找比轴值大的元素,找到为止,然后交换两个数,重复此过程,知道左侧的下标和右侧的下标重合,最后交换重合的位置和轴元素(注意需要先将轴元素和最前面的元素交换)
(2)挖坑法
int PartSort2(int* a, int left, int right)
{
int tmp = a[left];
int index = left;
while(left<right)
{
while(left<right && a[right]>=tmp)
{
right--;
}
a[index] = a[right];
index = right;
while(left<right && a[left]<=tmp)
{
left++;
}
a[index] = a[left];
index = left;
}
a[index] = tmp;
return index;
}
void QuickSort(int* a, int left, int right)
{
if(left >= right)
return;
int mid = PartSort2(a,left,right);
QuickSort(a,left,mid-1);
QuickSort(a,mid+1,right);
}
解释:这个版本的思路是先行将轴值拿出来,把原来的位置变成一个坑,然后从右侧开始向左找比轴值小的元素,找到后填入之前的坑中,自己的位置变成坑的位置,然后左侧向右找比轴值大的元素,同样的操作,直至左右相遇,将轴值填入坑中
(3)前后指针法
int PartSort3(int* a, int left, int right)
{
int key = a[left];
int prev = left;
int cur = prev+1;
while(cur<=right)
{
if(a[cur]<key)
{
prev++;
if(prev!=cur)
swap(&a[prev],&a[cur]);
++cur;
}
else if(a[cur]>=key)
{
cur++;
}
}
swap(&a[prev],&a[left]);
return prev;
}
void QuickSort(int* a, int left, int right)
{
if(left >= right)
return;
int mid = PartSort3(a,left,right);
QuickSort(a,left,mid-1);
QuickSort(a,mid+1,right);
}
解释:这个方法的思路是用两个变量cur和prev来充当指针,起始时prev放在left,cur比prev大1
调整时,如果cur指向的值小于轴值,那么prev++并把prev和cur所指的值交换,如果cur所指的值大于轴值就只++cur,等到cur走到末尾,交换轴值和prev所指位置的值。这个过程中我们可以看到cur只有在遇到比轴值大的数的时候才会和prev拉开身位,等到prev找到比轴值小的值时,prev的下一个一定是比轴值更大的元素
(4)非递归实现快排
以上三种方法都是通过递归实现的,我们再进行递归操作时,如果数据量很大,递归深度过深,很容易出现爆栈的情况,这时候就不得不调整为非递归的形式
在这里,我们使用栈这种数据结构来模拟递归的过程,其先入后出的特点很符合递归的特点
栈的实现
typedef struct stack{
int* a;
int top;
int size;
}stack;
void init_stack(stack* st)
{
st->size = 4;
st->a = (int*)malloc(sizeof(int)*st->size);
st->top = 0;
}
void push_back(stack* st,int x)
{
if(st->top >= st->size)
{
st->size *=2;
st->a = (int*)realloc(st->a,sizeof(int)*st->size);
}
st->a[st->top++] = x;
}
bool stack_empty(stack*st)
{
if(st->top==0)
return true;
else
return false;
}
void stack_pop(stack* st)
{
st->top--;
}
int stack_top(stack* st)
{
return st->a[st->top-1];
}
void swap(int* a, int* b)
{
int c = *a;
*a = *b;
*b = c;
}
快排的实现(partsort3同先后指针法)
void QuickSortNonR(int* a, int left, int right)
{
stack* st = (stack*)malloc(sizeof(stack));
init_stack(st);
push_back(st,right);
push_back(st,left);
while(!stack_empty(st))
{
int left_tmp = stack_top(st);
stack_pop(st);
int right_tmp = stack_top(st);
stack_pop(st);
int mid = PartSort3(a,left_tmp,right_tmp);
if(mid+1<right_tmp)
{
push_back(st,right_tmp);
push_back(st,mid+1);
}
if(mid-1>left_tmp)
{
push_back(st,mid-1);
push_back(st,left_tmp);
}
}
}
解释:先将右边界入栈,再把左边界入栈,之后进入循环,取两个边界放入单次排序的函数中,然后得到轴的下标,并再次入栈,直至栈为空
3.复杂度分析
在以上方法中我们都选择的是最左侧的值为轴值,这样是不合理的,如果排序的数列已经接近有序,就会不可避免地将大量数据排在同一侧,大大增加了复杂度,所以我们可以使用rand()函数进行随机选择轴值,这样可以应对更多种情况。
此外,在面对数据量较多时,可以在递归到一定程度时使用插入排序,进行优化,减少递归深度,因为在数据量较小时插入排序等排序方法和快速排序差异不明显,而对栈的开销更少,性能更高
三、复杂度稳定性的对比
对于稳定不稳定的讨论,举个例子,就是大小一样的元素他们的相对位置在排序前后会不会发生改变,发生改变就是不稳定的,不改变就是稳定的,大家可以尝试不同的例子进行体会
总结
好的,这节结束,对于基本的排序方法我们都能有一个大概的了解,但是别人说多讲多,都不如自己敲一敲代码尝试一下,只有真正靠自己写出来代码,知识才是落到实处,加油吧,希望能有所收获
图片来自网络


515

被折叠的 条评论
为什么被折叠?



