1.1排序概念及运用
排序:所谓排序,就是使一串记录,按照其中的某个或某些关键字的大小,递增或递减的排序起来的操作。
1.2分类

在之前我们已经学过冒泡排序和堆排序,所以这次我们只讲剩下的几种排序。
上面的排序都属于比较排序,我还会讲一种非比较排序:计数排序
1.3直接插入排序
直接插入排序是一种简单的插入排序,其基本思想是:把待排序的记录按其关键码值的大小逐个插入到一个已经排好序的有序序列中,直到所有的记录插入完为止,得到一个新的有序序列。
其实直接插入排序的思想就像我们打扑克牌一样,是一个道理。

就拿这个数组举例,实现直接插入排序我们要定义两个临时变量end和tmp
1.end初始在数组的第一个位置,tmp在end的下一个位置
2.拿tmp位置的值和end位置的值进行比较,如果end位置的值比tmp位置的大,就把此时的end位置空出来,end向前移一位,拿tmp的值一直向前比较,直到end位置的值比tmp的值小,那么end的下一个位置就是tmp的值所在的位置,跳出循环,将其赋值即可
实现这段代码需要注意:
1.外面这层循环控制的是数组需要比较的次数,比如这个数组有六个数据,我们只需要执行五次操作即可,把五个数据放在对的位置,剩下的最后一个数据就会在正确的位置
2.内层循环控制的是一个数据要放在正确的位置需要比较的次数,while每次循环结束,就代表找到了一个数据正确的位置
3.end和tmp不能定义在循环外边,因为end和tmp的位置每次循环之后都要改变
下面是代码演示:
void InsertSort(int* arr, int n)
{
//控制整个数组需要比较的次数
for (int i = 0; i < n - 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(n)数组有序且为升序
1.4希尔排序
希尔排序法又称缩小增量法。希尔排序法的基本思想:先选定一个整数(通常是gap=n/3+1),把待排序文件所有记录分成各组,所有的距离相等分在同一组内,并对每一组内的记录进行排序,然后gap=gap/3+1得到下一个整数,再将数组分成各组,进行插入排序,当gap=1时,就相当于直接插入排序。
它是在直接插入排序算法的基础上进行改进而来的,综合来说它的效率肯定是要高于直接插入排序算法的。


第一趟和第二趟操作叫做预排序,当gap=1时就是直接插入排序,当gap=1时数组已经相对有序了。
gap的变化不是一定为gap/3+1,你也可以换成其它值,但要保证最后一次gap要等与1。
下面是代码演示:
void ShellSort(int* arr, int n)
{
int gap = n;
//预排序的次数+直接插入排序
while (gap > 1)
{
gap = gap / 3 + 1;
//控制整个数组需要比较的次数
for (int i = 0; i < n - 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;
}
}
}注意:里面的代码跟直接插入排序基本相同,不过+1,-1,变成了+gap,-gap。
另外如果你选择gap=gap/2,注意不能+1,否则会死循环。
时间复杂度:

只要记住正常情况希尔排序的时间复杂度约为n^1.3即可,最好情况下可以达到nlogn。
1.5直接选择排序
基本思想:每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,知道全部待排序的数据元素排完。

我以这个数组为例,将基本思想实现起来我们可以定义begin和end两个变量,表示我们要在哪个区间里找最大值和最小值,然后定义maxi和mini表示最大值和最小值,起始位置在begin处,在相应区间找到最大值和最小值之后,让maxi位置的值与end位置的值进行交换,mini位置的值与begin位置的值进行交换 ,之后再将begin++,end--,改变区间,就这样一直循环,循环停止的条件就是begin=end或begin>end,此时区间内只有一个数据或没有数据,就可以实现数组的升序排序。
简而言之就是在一个区间内,遍历整个数组找出最小值和最大值,与begin和end位置的值进行交换。
下面是代码演示:
void SelectSort(int* arr, int n)
{
int begin = 0;
int end = n-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;
}
}
//如果起始位置就是最大值时
if (maxi == begin)
{
maxi = mini;
}
Swap(&arr[mini], &arr[begin]);
Swap(&arr[maxi], &arr[end]);
begin++;
end--;
}
}注意:这个排序最关键的就是如果起始位置就是最大值,我们要让maxi移到mini的位置,这样mini位置的值与begin交换后最大值就转移到mini的位置,也就是此时maxi位置,再让maxi位置的值和end交换后最大值还会回到区间的末尾。
时间复杂度:O(n^2)
1.6快速排序(递归)
快速排序是Hoare于1962提出的一种二叉树结构的交换排序方法,其基本思想为:任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后左右子序列重复该过程,直到所有元素凑排列在相应的位置上为止。

快速排序是需要用递归来实现的,每次递归都会根据当前的key值划分成不同的区间,直到划分后的数组只有一个数据,也就是left=right,递归就要停止。
以这个数组为例,我们先取9作为key值,定义两个变量left和right,让left和right位置的值与key值进行比较,left来寻找比key大的值,right来寻找比key小的值,如果找到了,就让left和right位置的值进行交换,最后交换right和key位置的值,right位置就是key在数组中所在的位置,返回right的下标即可。
快速排序的关键就是找到key所在的位置,故这个功能需要单独封装成一个函数。
下面是代码演示:
Hoare版本:
//hoare版本
int FindKey(int* arr, int left, int right)
{
int key = left;
left++;
//=是为了解决特殊情况:left与right相遇位置的值要比基准点的值大,如果此时结束循环交换基准点与right的值
//就会导致左边的值不满足都比基准点小
while (left <= right)
{
//left:从左往右找比基准值大的值
while(left <= right && arr[left] < arr[key])
{
left++;
}
//right:从右往左找比基准值小的值
while(left <= right && arr[right] > arr[key])
{
right--;
}
//交换此时的left和right的值
if(left <= right)
{
Swap(&arr[left++], &arr[right++]);
}
}
//将基准点与此时right位置的值进行交换,使其放在合适的位置
Swap(&arr[key], &arr[right]);
return right;
}
void QuickSort(int* arr, int left, int right)
{
//递归停止的条件
if (left >= right)
{
return;
}
int keyi = FindKey2(arr, left, right);
//将一个数组分成多个数组进行排序
QuickSort(arr, left, keyi - 1);
QuickSort(arr, keyi + 1, right);
}注意:
1.快速排序与前面的排序不同,需要传入的参数是left和right,也就是数组最左边的下标和最右边的下标。 2.循环条件是left<=right,=的情况也要判断是因为如果相遇位置的值比key值要大,而此时如果停止循环,交换key和right位置的值,就会导致比key大的值跑到前面去,就不符合key左边的数据都比key小,右边的数据都比key大。
3.交换left和right的值后循环依旧没有停止!!!,left和right依旧还需要去判断,接着走,这是理解快速排序整个过程的关键。
3.注意递归时传入的区间是key-1和key+1,就是以key的位置为界,把整个数组分成两个数组
lumuto前后指针法:
基本思想:创建两个前后变量prev和cur,cur从左往右找基准值小的值,找到后,prev和cur交换
1>cur指向的数据比基准值要小: prev++,prev和cur交换
2>cur指向的数据比基准值要大
这两种情况不管哪一个发生cur都要向后走

依旧以这个数组为例,选取9作为key,prev在起始位置,cur在其下一个位置,按照上面的思想执行,一次循环结束后,交换prev和key的值,prev所在的位置就是基准值所在的位置。
下面是代码演示:
int FindKey2(int* arr, int left, int right)
{
int key = left;
int prev = left;
int cur = prev + 1;
while (cur <= right)
{
//从左往右找比基准值小的值
if (arr[cur] < arr[key] && ++prev != cur)
{
Swap(&arr[prev], &arr[cur]);
}
cur++;
}
Swap(&arr[prev], &arr[key]);
return prev;
}注意:
1.如果prev++后和cur在同一位置,就不需要进行交换,这样可以减少交换的次数
2.循环结束的条件是cur>right,因为最后一个值也需要进行比较
前后指针法相较于Hoare版本的方法代码量会少一些,也更好理解
三路划分
这个方法主要针对于数组中含有大量重复数据而设计的,可以使重复的数据集中在数组中间。
基本思想:定义begin和end来记录原始的数组左侧和右侧,取最左边的数据作为基准值key,定义cur在左侧第二个位置,cur向后遍历数组,如果cur位置的值比key要小,交换cur和left,然后cur++,left++,如果cur位置的值比key要大,交换cur和right,right--,因为并不知道交换过来的值是否比key大还是小,故cur不动。因此其实在循环中交换过后还需要在比较一次cur和key,如果遇到相同的值,cur++。循环结束后,left所在的位置就是基准值所在的位置,以left为界即可。
下面是代码演示:
void QuickSort3(int* arr, int left, int right)
{
//递归停止的条件
if (left >= right)
{
return;
}
//三路划分
//主要针对于数组中有大量与基准值相同的值或数组的值全部一样
//正常使用会比hoare和前后指针法效率低一些
int begin = left;
int end = right;
int key = left;
int cur = left + 1;
while (cur <= right)
{
if (arr[cur] < arr[key])
{
Swap(&arr[cur], &arr[left]);
cur++;
left++;
}
else if (arr[cur] > arr[key])
{
Swap(&arr[cur], &arr[right]);
right--;//因为不知道交换过来的值是否比基准值大还是小,故cur不动
}
//遇到和基准值相等的值
else
{
cur++;
}
}
//[begin left-1] [left right] [right+1 end]
//将一个数组分成多个数组进行排序
QuickSort(arr, begin, left-1);
QuickSort(arr, right+1, end);
}三路划分其实相当于Hoare版本和前后指针法的结合,只是做了一点修改。
1.7快速排序(非递归)
基本思想:非递归版本的快速排序需要借助数据结构栈来实现,将每次被分割的数组的左右区间放入栈中,然后取两次栈顶的元素没也就是取出数组的左右区间,取出后删除栈顶元素,然后利用上面所写的三种方法之一找到基准值的位置,然后判断要放入的新数组是否是正确的,即数组的左边要小于右边,符合标准才能放入栈中,一直循环,直到栈被取空了,此时数组就会变为有序的。
下面是代码演示:
void QuickSort2(int* arr, int left, int right)
{
//用栈来实现非递归方式的快速排序
ST st;
STInit(&st);
STPush(&st, right);
STPush(&st, left);
while (!STEmpty(&st))
{
int begin = STTop(&st);
STPop(&st);
int end = STTop(&st);
STPop(&st);
//把基准值放在合适的位置
int keyi = FindKey2(arr, begin, end);
//判断新的数组是否符合标准
if (begin < keyi - 1)
{
STPush(&st, keyi - 1);
STPush(&st, begin);
}
if (keyi+1 < end)
{
STPush(&st, end);
STPush(&st, keyi+1);
}
}
STDestory(&st);
}注意:
1.要开始就向栈中放入当前数组的区间,不然栈开始就是空的
2.一定要判断要存入的区间是否符合标准!!!,可以自己画一画整个过程,确实会有不符合标准的区间
1.8归并排序(递归)
归并排序是建立在归并操作上的一种有效的排序算法,该算法是采用分治法的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,成为二路归并。归并排序核心步骤:

上半部分是分解操作,下半部分是合并操作。
基本思想:通过递归将数组分成只含有一个数据的数组,再通过合并两个有序数组的思想将数组两两合并,最后当所有数组合并完成后,数组中的数据就是排好序的升序数组。
合并两个有序数组:定义begin1,end1来代表第一个数组的左右区间,begin2,end2来代表第二个数组的左右区间,遍历两个数组,将当前位置的值进行比较,那个小就把那个数组的数据放入临时数组中,begin1或begin2++,循环停止的条件是其中一个数组已经将所有数据放入临时数组中,循环停止后再将另一个数组剩下的数据放入临时数组中,这样就完成了两个有序数组的合并。
下面是代码演示:
void _MergeSort(int* arr, int left, int right, int* tmp)
{
//分解
//递归结束条件
if (left >= right)
{
return;
}
int mid = (left + right) / 2;
//不断将数组拆分为两个区间:[left,mid] [mid+1,right]
_MergeSort(arr, left, mid, tmp);
_MergeSort(arr, mid+1, right, tmp);
//合并
//利用合并两个有序数组算法的思想
int begin1 = left, end1 = mid;
int begin2 = mid + 1, end2 = right;
int index = begin1;//不等于0是为了避免每次排序都从0开始
while (begin1 <= end1 && begin2 <= end2)
{
if (arr[begin1] < arr[begin2])
{
tmp[index++] = arr[begin1++];
}
else
{
tmp[index++] = arr[begin2++];
}
}
//左序列的数没有排完
//右序列的数没有排完
while (begin1 <= end1)
{
tmp[index++] = arr[begin1++];
}
while (begin2 <= end2)
{
tmp[index++] = arr[begin2++];
}
//将临时数组排好序的数传入原数组中
for (int i = left; i <= right; i++)
{
arr[i] = tmp[i];
}
}
void MergeSort(int* arr, int n)
{
//利用合并两个有序数组算法的思想
int* tmp = (int*)malloc(sizeof(int) * n);
_MergeSort(arr, 0, n - 1, tmp);
free(tmp);
tmp = NULL;
}注意:
1.传过去的参数不仅有left和right,还有临时数组,因为如果把临时数组创建在分解和合并函数中,每次递归都会开辟新的空间,就无法做到排序。
2.index初始值不能为0,否则递归过程中排序都会从0的位置开始,而有的数组起始位置并不是0。
3.注意循环结束的条件,是&&而不是||,只要有一个数组已经把所有数据放入到临时数组中,循环就要停止。
时间复杂度:O(nlogn)
1.9归并排序(非递归)

基本思想:在非递归归并排序中,我们用gap来记录每个数组中有几个数据,而循环停止的条件就是gap等于原数组的长度,不过和递归版本不同的是我们才开始就将整个数组划分成一个个只含有一个数据的数组,由下向上进行合并,没有了分解的过程,直接开始合并,然后依旧利用合并两个有序数组的思想将数组排好序放入临时数组中,最后将临时数组的数据复制到原数组中。
下面是代码演示:
void MergeSort2(int* arr, int n)
{
int* tmp = (int*)malloc(sizeof(int) * n);
if (tmp == NULL)
{
perror("malloc fail!");
exit(1);
}
int gap = 1;//定义gap表示此时每个数组有几个数据
while (gap < n)
{
for (int i = 0; i < n; i += 2 * gap)
{
int begin1 = i, end1 = i + gap - 1;
int begin2 = i + gap, end2 = i + 2 * gap - 1;
int index = begin1;
//若数组长度为奇数
if (begin2 >= n)
{
break;//表示没有右序列
}
if (end2 >= n)
{
end2 = n - 1;//表示此数组数据个数未满gap个
}
while (begin1 <= end1 && begin2 <= end2)
{
if (arr[begin1] < arr[begin2])
{
tmp[index++] = arr[begin1++];
}
else
{
tmp[index++] = arr[begin2++];
}
}
while (begin1 <= end1)
{
tmp[index++] = arr[begin1++];
}
while (begin2 <= end2)
{
tmp[index++] = arr[begin2++];
}
//注意!!!
for (int j = i; j <=end2; j++)
{
arr[j] = tmp[j];
}
}
gap =gap * 2;//两两合并,故×2
}
free(tmp);
tmp = NULL;
}注意:
1.因为不需要递归,所以传入的参数和之前一样,只有数组和数组的长度
2.注意begin1,end1和begin2,end2的取值,还有i的取值,因为begin1,end1和begin2,end2的位置每次的取值是有间隔的
3.如果数组长度为奇数,一定要判断最后那个数据,也就是判断begin2和end2,理由代码中我已经写了,这个一定要判断!!!,否则会越界访问,因为归并排序是两两进行合并的,所以我们要判断最后一个数据
4.要每一次循环都把tmp数组中排好的数据复制到原数组中,因为如果数组长度是奇数,我们传入tmp数组中的数据只有n-1个(因为是两两合并,而最后一组只有一个,会先跳过它),而如果在循环结束后再把tmp数组中的数据复制到原数组中,会有一个数据是随机数,就会导致排序失败。
2.0计数排序(非比较排序)
计数排序又称为鸽巢原理,是对哈希直接定址法的变形应用。操作步骤: 1>统计相同元素出现次数
2>根据统计的结果将序列回收到原来的序列中

上面的步骤实现用这张图来表示,最上面就是要排序的数组,中间就是创建了一个临时数组来记录每个数据出现的次数,最后就根据临时数组将所有的数据按顺序复制到原数组中。
基本思想:先找出原数组中的最大值和最小值,将最大值和最小值相减再+1就得到了临时数组的长度,再根据长度创建临时数组,将原数组中出现的各个数据出现的次数存入临时数组中,然后将临时数组中存入的数据转化为原数据存入到原数组中,这样就完成了排序。
下面是代码演示:
void CountSort(int* arr, int n)
{
int min = 0;
int max = 0;
for (int i = 0; i < n; i++)
{
if (arr[i] < min)
{
min = arr[i];
}
if (arr[i] > max)
{
max = arr[i];
}
}
//得到count数组的大小
int range = (max - min) + 1;
int* count = (int*)malloc(sizeof(int) * range);
if (count == NULL)
{
perror("malloc fail!");
exit(1);
}
memset(count, 0, sizeof(int) * range);
//将arr各个数出现的次数存入count数组中
for (int i = 0; i < n; i++)
{
count[arr[i] - min]++;
}
int index = 0;
//将count中存入的数据转化为原数据存入到arr数组中
for (int i = 0; i < range; i++)
{
while (count[i]--)
{
arr[index++] = i + min;
}
}
}注意: 1.创建完临时数组后要将数组中的数据初始化为0,或者用realloc也行,因为我们要用的是数组原本的数据,而不是将一个数组的数据复制到临时数组中。
2.将arr各个数出现的次数存入count数组中这步操作中,count[arr[i]-min]++中减min是因为原数组中的数据可能并不为0到9,可能为100到109,但是我们不会创建109个空间,那太浪费了。而arr[i]-min代表的就是原数组的数据在临时数组中所处的位置,也就是下标
3.将count中存入的数据转化为原数据存入到arr数组中这步操作中,要将一个位置的所有数据存入原数组后再到下一个位置,而且取出的数据要加上min,因为我们的上一步操作减去了min
缺点:很明显,计数排序只能排一些数据相差不大的数据,如果数据间相差太大,就需要创建很大的空间,空间会造成很大一部分的浪费,所以计数排序的使用是有局限性的。
2.1总结
1.在这些排序中,直接插入排序和直接选择排序我们了解一下就行,因为它们的应用意义不大,有比它们效率高得多的排序,需要记牢的有希尔,堆,快速,归并排序。尤其是快速排序,因为快速排序应用意义很大,所以对快速排序的研究也比其它的排序要更深。
2.实现每一种排序都要检测是否能完成排序工作,要知道它们实现的基本思想。