文章目录
排序
一、排序概念
排序:所谓排序,就是使一串记录,按照其中的某个或某些关键字的大小,递增或递减的排列起来的操作。
二、排序的种类
1.插入排序
插入排序的核心思想是:设置一个有序区,并将后面的数不断与有序区的数进行比较,然后插入合适的位置中。
代码如下(直接插入排序):
void InsertSort(int* a, int end)//end是下标
{
for (int i = 1; i <= end; i++)
{
int tmp = a[i];
int k = i;
for (int j = i - 1; j >= 0; j--)
{
if (tmp < a[j])
{
Swap(&a[j], &a[k]);
k = j;
}
}
}
}
当要进行排序的序列是接近有序的时候,用插入排序的时间复杂度接近于O(N)。对于最坏的情况逆序而言,其时间复杂度为O(N^2)。于是人们思考要如何使一个序列变得接近有序,这时希尔排序就出现了:
代码如下(希尔排序):
void ShellSort(int* a, int end)//希尔排序
{
int gap = end + 1;
while(gap>1){
//比如gap=2时,/3直接变成0,没有进行gap=1的插入
gap = (gap / 3+ 1);
for (int i = gap; i <= end; i++)
{
int end = i;
while (end >=gap)
{
if (a[end ] < a[end- gap ])
{
Swap(&a[end], &a[end - gap]);
end -= gap;
}
else {
break;
}
}
}
}
}
希尔排序巧妙地使用了预分组的思想,将序列变得接近有序。
希尔排序通过设置不同的gap进行多次分组,然后分别在组内进行插入排序。
随着gap的逐渐减小,序列变得越来越接近有序。最后当gap=1时,最后一趟排序过称其实就变成了直接插入排序,而此时序列已经接近有序,所以最后一趟的时间复杂度就是O(N)。
2.选择排序
代码如下(选择排序):
void SelectSort(int* a, int end)//选择排序(优化版:一次选两个数)
{
int maxIndex = 0;//找到最大值的下标
int minIndex = 0;//找到最小值的下标
int left = 0;//左边界
int right = end;//右边界
while (left < right)//当左边界小于右边界时,循环继续
{
for (int j = left; j <= right; j++)
//每次挑选两个数之后左右边界都会发生变化
//左边界++,右边界--
{
maxIndex = j;
minIndex = j;
if (a[maxIndex] < a[j])
{
maxIndex = j;
}
if (a[minIndex] > a[j])
{
minIndex = j;
}
}
Swap(&a[maxIndex], &a[right--]);
Swap(&a[minIndex], &a[left++]);
}
}
选择排序:通过每次对序列进行遍历挑选出最小和最大的数,并分别与左右边界的数字进行交换,最后实现有序。同时选择排序的缺点也十分明显,就是当序列接近有序甚至是有序的时候,选择排序还是通过一次次遍历进行选数,从其时间复杂度的角度上就比不上同一梯队的直接插入排序和冒泡排序。
选择排序还有变种鸡尾酒排序,感兴趣的铁子可以去看看(这里不做介绍)。
代码如下(堆排序):
void AdjustDown(int* a, int parent,int end)//排升序,要建大堆
{
int child = parent * 2 + 1;
while (child < end)
{
if (child + 1 < end && a[child] < a[child + 1])
{
child++;
}
if (a[child] > a[parent])
{
Swap(&a[child], &a[parent]);
parent = child;
child = parent * 2 + 1;
}
else {
break;
}
}
}
void HeapSort(int* a, int end)//堆排序
{
int parent = (end - 1) / 2;
for (int i = 0; i < parent; i++)//建大堆
{
AdjustDown(a, parent - i, end );
}
for (int j =0 ;j<end;j++)//每次把堆顶的数换到数组尾部
{
Swap(&a[0], &a[end - j]);
AdjustDown(a, 0, end - j);
}
}
向下调整法:以建大根堆(root结点为例),先对root的两个孩子结点进行比较,用那个大的结点与父亲结点(也就是根节点)进行比较,如果比root大,就将父亲节点与那个大的孩子结点进行交换,通过一次次的重复比较,之前的那个root结点要么到达了叶子节点,要么就大于他现在的两个孩子结点。
建堆:堆本身就是一颗完全二叉树,堆有大根堆和小根堆之分。大根堆就是任意父亲节点大于与之相连的孩子节点,所以大根堆的root结点就是最大值。当我们得到一串无序序列时,我们可以去寻找其最后一个父亲结点,通过向下调整法进行向下调整,将小的数交换到下面。并以最后一个父亲结点为起点,一步一步进行调整直到调整到root结点(最顶端的结点),这样大根堆就建好了。
选数:根据大根堆的特性,root结点的数字最大,通过将他与最后的叶子结点(边界)进行交换,将最大的数字就排到了最后,小的数字就到达了堆顶,然后将排好的最大数字除外,重新进行向下调整,然后将次大的数排到边界,依此类推,直到最后的root结点,序列就有序了。
注意!!!排升序的时候要建大堆,排降序的时候要建小堆。原因时:以升序为例,如果建小堆,那么我们每次可以选出最小的数,但是选出来之后,整棵树的父子关系就完全乱套了,那我们就要重新建堆,建堆的时间复杂度为O(N),若每次都要重新建堆,其时间复杂度就达到了O(N^2),那还不如直接选择排序。(费时费力,代码都要敲半天)
3.交换排序
代码如下(冒泡排序):
void BubbleSort(int* a, int end)
{
for (int i = 0; i <= end; i++)
{
for (int j = 1; j <= end - i; j++)
{
if (a[j - 1] > a[j])
{
Swap(&a[j - 1], &a[j]);
}
}
}
}
冒泡排序:(升序)就是通过一次次相邻元素的比较,把较大的数字排到后面,较小的数字排到前面。这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端(升序或降序排列),就如同碳酸饮料中二氧化碳的气泡最终会上浮到顶端一样,故名“冒泡排序”。
** 快速排序
快速排序:运用了分治思想,每次都选出一个key(一般是最左边或者最右边),通过一系列的操作使这个key的到达它的正确位置。即(升序)它左边的数字都小于它,它右边的数字都大于它,这样这个key的位置就不会再发生改变了。同样的,key值将原本的一个序列分成了两个序列,这个时候我们就分别再重新选取key值,将它排好,又将两边序列分别分成了两段,以此类推,最后序列就可以达到有序。
1)将key值放到正确位置的三种方法:
(1)左右指针法(升序)
如下图,p为key值,若选取最右边为基准值(key),那么指向最左边的指针就应该先向右移动,去找比基准值大的数,如果找到了,再移动右边的指针,向左去找比基准值小的数,找到后,将两者交换,直到左右指针相遇,此时将基准值与相遇的地点进行交换,那么基准值左边的数字都比它小,右边的数字都比它大。
注意!!如果把最右边的数定为基准值的话,一定要先移动左边的指针。否则如下图:(升序)最后那个比基准值小的4被交换到了最右边。
(2)挖坑法
同样的先选取最左边或者最右边为基准值(这里选取最左边),将基准值保存在一个临时变量中,这样原存放key值的地方就空了,然后右边的R先走去找比key小的数,找到了就把它换到key的坑位,这时候右边小人的这个位置上因为数字被换走了,就出现了新的坑位,然后左边的L向右走去找比key大的数,找到后将它填到右边R的那个坑位。以此类推,当二者相遇时直接将key填到R和L脚下的坑里面。
(3)前后指针法
先取基准值(此处为最右边),cur指针向右去找比key小的数字,如果找到了,prev就加一,同时交换两个指针所指向的数字。以此类推,最后当cur指针指向最右边时,prev加一,同时将key值和prev所指向的数字进行交换。(这个方法中的prev和cur可以同时和key在同一侧)
三种方法的代码如下:
int PartSort1(int* a, int begin, int end)//左右指针法
{
Swap(&a[begin], &a[SeletMidIndex(a, begin, end)]);
int key = begin;
int left = begin;//最左边界
int right = end;//最右边界
while (begin < end) {
while (end > begin && a[end] >= a[key])//end>=begin等号不能要,有等号数组就会越界
{
end--;//key选取的最左边,则从右边开始找大于a[key]的
}
while (begin < end && a[begin] <= a[key])//end>=begin等号不能要
{
begin++;//右边找完后,在从左边开始找小于a[key]的
}
Swap(&a[begin], &a[end]);
}
Swap(&a[begin], &a[key]);
return begin;
}
int PartSort2(int* a, int begin, int end)//前后指针法
{
Swap(&a[begin], &a[SeletMidIndex(a, begin, end)]);
int key = begin;
int cur = begin + 1;//从key的右边开始
int prev = begin;
while (cur <= end) {
//找到了小于a[key]的数,就把prev++,然后交换
if (a[cur] < a[key])
{
++prev;
Swap(&a[cur], &