什么是排序
排序的概念
先引入排序的相关概念
排序:所谓排序,就是使一串记录,按照其中的某个或某些关键字的大小,递增或递减的排列起来的操作。
稳定性:假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,r[i]=r[j],且r[i]在r[j]之前,而在排序后的序列中,r[i]仍在r[j]之前,则称这种排序算法是稳定的;否则称为不稳定的。
内部排序:数据元素全部放在内存中的排序。
外部排序:数据元素太多不能同时放在内存中,根据排序过程的要求不能在内外存之间移动数据的排序。
内部排序
大家阅读完以上概念后可能对稳定性,内外排序还是不理解,我就举一个栗子帮助大家来理解什么是排序。😦😦
💚
什么是稳定性:假如我们在参加一场竞赛,竞赛规则是得分高者名次高,同分数者用时短者名次高。在本次比赛中你获得了100分的成绩用时一小时,而还有一名同学取得了100的成绩用时1小时20分钟,考官将每一名参赛者的信息(信息包含姓名,考号,成绩等等)按交卷时间的先后顺序录入到一个顺序表中,最后对顺序表中的数据以成绩为关键字进行排序(注意关键字就只有成绩,和时间没关系),用于你用时一小时而另一个人比你晚二十分钟在排名后你的名次靠前,在排序过程中你和另一个人的位置是相对的,不曾改变,这种排序的逻辑就是稳定的。如果说最后另一个人最后名次比你高,就说明考官用的排序逻辑不稳定。按照比赛规则还需进行排序。
💚
内/外部排序:程序在运行时都要加载到内存,对于容量较小的数据能够一次性加载到内存进行排序是内部排序,但是对于容量特别特别大的数据就需要在内存和硬盘之间对数据进行移动,每次对一部分数据进行排序,通过多次排序得到结果就是外部排序
💚
基本的排序方法
排序实现
本文的排序都是升序,所以用升序的逻辑进行解释
冒泡排序
先看动图💃💃💃💃💃💃💃
void BubbleSort(int* arr, int N)
{
for (int i = 0; i < N - 1; i++)
{
for (int j = 0; j < N - 1 - i; j++)
{
if (arr[j] > arr[j + 1])
{
int tmp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = tmp;
}
}
}
}
逻辑解释:冒泡排序由两层循环构成,内层循环对数据进行两两比较,通过n-1-i次比较(每当一个元素达到正确位置后就不再需要拿它进行比较所以减去i(i表示有i个元素达到正确位置))把一个数据移动到正确位置,外层循环每次循环把一个数据移动到正确位置,对于一个有n个元素的数组来说需要把n个元素移动到正确位置,在当我们把n-1个元素移动到正确位置后,最后一个元素也相对的到了正确位置,所以外层循环次数为n-1;
逻辑改进
void BubbleSort(int* arr, int N)
{
for (int i = 0; i < N - 1; i++)
{
int flag = 1;
for (int j = 0; j < N - 1 - i; j++)
{
if (arr[j] > arr[j + 1])
{
int tmp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = tmp;
flag=0;
}
}
if(flag==1)
{
break;//flag等于1说明没有进行交换,说明数据已经有序
}
}
}
简单选择排序
基本思想: 每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到全部待排序的数据元素排完 。
void SelectSort(int* arr, int N)
{
int begin = 0, end = N - 1;
while (begin < end)
{
int min = begin;
for (int i = begin + 1; i <= end; i++)
{
if (arr[i] < arr[min])
{
min = i;
}
}
swap(&arr[begin], &arr[min]);
begin++;
}
}
逻辑解释:在begin到end范围内找出最小值并用min记录它的下标,最后与begin位置的元素交换位置。通过N次交换获得一个有序序列。
交换排序
💚本质上与选择排序一样,只不过每次是找出最大值和最小值进行交换💚
基本思想:所谓交换,就是根据序列中两个记录键值的比较结果来对换这两个记录在序列中的位置,交换排序的特点是:将键值较大的记录向序列的尾部移动,键值较小的记录向序列的前部移动。
//swap交换函数,下面就不再展示
void swap(int* x, int* y)
{
int tmp = *x;
*x = *y;
*y = tmp;
}
void SwapSort(int* arr, int N)
{
int begin = 0,end = N - 1;
while (begin < end)
{
int max = begin, min = begin;
for (int i = begin+1; i <= end; i++)
{
if (arr[i] > arr[max])
{
max = i;
}
if (arr[i] < arr[min])
{
min = i;
}
}
swap(&arr[begin],&arr[min]);
if (max == begin)
{
max = min;
}
swap(&arr[end], &arr[max]);
end--;
begin++;
}
}
逻辑解释:选择排序每次在begin到end范围内选出最大值和最小值并且用下标min和max记录最小值和最大值的位置,最后min位置的数与begin位置的数交换,max位置的数与end位置的数交换(过程中如果最大值在begin位置,而begin位置的数与min位置的数已经交换过的话就说明最大值的位置现在在min位置,把最大值的标记换为min的下标)。💃💃💃💃
直接插入排序
把待排序的记录按其关键码值的大小逐个插入到一个已经排好序的有序序列中,直到所有的记录插入完为止,得到一个新的有序序列 。
当插入第i(i>=1)个元素时,前面的array[0],array[1],····array[i-1]已经排好序,此时用array[i]的排序码与array[i-1],array[i-2],…的排序码顺序进行比较,找到插入位置即将array[i]插入,原来位置上的元素顺序后移
先看动图💃💃💃💃💃💃💃
看完动图后有没有觉得直接插入排序逻辑和我们打扑克时的手法一样,从牌堆里抓牌再排序,每次抓到一张新的牌在插入之前,之前的牌已经是有序的了,再把这张新的牌通过直接插入排序插入到正确位置,使当前抓到的牌有序。
void InsertSort(int* arr, int N)
{
// [0,end]有序,把end+1位置的插入到前序序列
// 控制[0,end+1]有序
for (int i = 0; i < N - 1; i++)
{
//循环条件n-1,假设依次抓17张牌,当我们抓到第一张牌时已经是有序的,而后面16张牌需要比较插入16次才能有序
int end = i;
int tmp = arr[end + 1];
while (end>=0)
{
if (tmp < arr[end])
{
arr[end + 1] = arr[end];//这是一个覆盖腾挪的过程
}
else
{
break;
}
end--;
}
//这里有两种情况
//一是找到了比它小的数,放到end+1位置;
//二是没有比它小的数,这时end=-1,end+1位置也就是arr[0]的位置
arr[end + 1] = tmp;
}
}
我们每次插入时 [0,end]已经有序,我们需要把end+1位置的元素插入到 [0,end],最后使得[0,end+1]有序。
希尔排序
希尔排序相当于直接插入排序plus,是直接插入排序的一种改进
希尔排序法又称缩小增量法。希尔排序法的基本思想是:先选定一个整数,把待排序文件中所有记录分成个组,所有距离为的记录分在同一组内,并对每一组内的记录进行排序。然后重复上述分组和排序的工作。当到达=1时,所有记录在统一组内排好序。
希尔排序的核心逻辑为两步
1,预排序(gap>1都是预排序)
2,直接插入排序
void ShellSort(int* arr, int N)
{
int gap = N;
while (gap>1)
{
gap = gap / 3 + 1;//+1保证最后一步对整体进行直接插入排序
//每gap步分为一组,分为gap组
for (int i = 0; i < gap; i++)
{
//对每一组分别进行直接插入排序
for (int j = i; j < N - gap; j += gap)
{
int end = j;
int tmp = arr[end + gap];
while (end >= 0)
{
if (tmp < arr[end])
{
arr[end + gap] = arr[end];
}
else
{
break;
}
end -= gap;
}
arr[end + gap] = tmp;
}
}
}
}
代码改进
void ShellSort2(int* arr, int N)
{
int gap = N;
while (gap > 1)
{
gap = gap / 3 + 1;
for (int j = 0; j < N - gap; j++)
{
//这里是多组并排,实质上还是对每组数据进行分别排序。
int end = j;
int tmp = arr[end + gap];
while (end >= 0)
{
if (tmp < arr[end])
{
arr[end + gap] = arr[end];
}
else
{
break;
}
end -= gap;
}
arr[end + gap] = tmp;
}
}
}
堆排序
在介绍堆排序之前先引入两个概念
堆的性质:堆中某个节点的值总是不大于或不小于其父节点的值;堆总是一棵完全二叉树。
小根堆:所有父节点小于等于孩子节点
大根堆:所有父节点大于等于孩子节点
如果想进行堆排,首先要有一个堆
这里介绍一种建堆的方法:向下建队算法
向下建队算法有一个前提条件一个节点的左右子树是一个堆,才能调整建堆。每一个堆的叶子节点的父节点的左右子树恰巧符合这个规则,那么就需要从最后一个叶子节点的父节点开始向前依次建堆就能实现一个堆的构建.
//如果堆排升序就建大堆
//如果堆排降序就建小堆
//建大堆
void AdjustDown(int* arr, int N, int parent)
{
int child = parent * 2 + 1;//求左孩子
while (child<N)
{
if (child+1 < N&&arr[child] < arr[child+1])
{
child++;//找两个子树中的较大的孩子
}
if (arr[child] > arr[parent])
{
//如果孩子比父亲大就交换,向下调整过程中把较大值向上挪动的过程
swap(&arr[child], &arr[parent]);
parent = child;
child = parent * 2 + 1;
}
else
{
break;
}
}
}
void HeapSort(int* arr, int N)
{
for (int i = (N - 1 - 1) / 2; i >= 0; i--)
{
AdjustDown(arr, N, i);
}
}
下图为建小堆示意图
堆排实现
知道父节点下标 lchild = parent * 2 + 1;//求左孩子
rchild = parent * 2 + 2;//求右孩子
知道孩子节点下标parent=(child-1)/2
void AdjustDown(int* arr, int N, int parent)
{
int child = parent * 2 + 1;//求左孩子
while (child<N)
{
if (child+1 < N&&arr[child] < arr[child+1])
{
child++;
}
if (arr[child] > arr[parent])
{
swap(&arr[child], &arr[parent]);
parent = child;
child = parent * 2 + 1;
}
else
{
break;
}
}
}
void HeapSort(int* arr, int N) {
for (int i = (N - 1 - 1) / 2; i >= 0; i--)
{
AdjustDown(arr, N, i);
}
int end = N - 1;
while (end > 0)
{
swap(arr, &arr[end]);
AdjustDown(arr, end, 0);
--end;
}
}
逻辑解释:当我们建好一个堆后,把0下标处元素与end下标处元素进行交换,每次把一个元素排序到正确位置
归并排序
递归版本
基本思想:
归并排序(MERGE-SORT)是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divide andConquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。 归并排序核心步骤
我们通过递归的方法完成归并排序,通过递归化大问题为小问题,化复杂为简单,我们现在面对的问题是使一个无序序列变成一个有序序列,怎么才能获得一个有序序列呢?获得两个有序序列通过归并就能获得一个有序序列,那怎么获得两个有序序列呢?获得四个有序序列通过归并就能获得两个有序序列。介就是递归,我们通常不需要考虑全局上是怎么实现的,我们只需要知道怎么解决一个小问题就能解决一个大问题。
void MergeSort(int* arr,int *tmp,int begin,int end)
{
if (begin >= end)
{
return;
}
int mid = (begin + end) / 2;
//获得两个有序序列
MergeSort(arr,tmp,begin, mid);
MergeSort(arr, tmp,mid+1,end);
//归并
int left1 = begin, right1 = mid;
int left2 = mid + 1, right2 = end;
int cur = begin;
while (left1 <= right1 && left2 <= right2)
{
if (arr[left1] < arr[left2])
{
tmp[cur++] = arr[left1++];
}
else
{
tmp[cur++] = arr[left2++];
}
}
while (left1 <= right1)
{
tmp[cur++] = arr[left1++];
}
while (left2 <= right2)
{
tmp[cur++] = arr[left2++];
}
memcpy(arr + begin, tmp + begin, (end - begin + 1)*sizeof(int));
}
void _MergeSort(int* arr,int N)
{
int* tmp = (int*)malloc(sizeof(int) * N);
if (tmp == NULL)
{
perror("malloc fail");
return;
}
MergeSort(arr,tmp,0,N-1);
free(tmp);
}
非递归版本
(本质上是模拟递归实现的)
void MergeSortNonR(int* arr, int n)
{
int* tmp = (int*)malloc(sizeof(int) * n);
if (tmp == NULL)
{
perror("malloc fail");
return;
}
int gap = 1;
while (gap < n) //控制每次gap个数据合并
{
for (int i = 0; i < n; i += 2*gap)//完成合并
{
int left1 = i, right1 = i + gap-1;
int left2 = i + gap, right2 = i + 2 * gap - 1;
int cur = i;
if (left2 >= n) //此情况下不存在第二组数据
{
break;
}
if (right2 >= n)//此情况下越界改变right2
{
right2= n-1;
}
while (left1 <= right1 && left2 <= right2)
{
if (arr[left1] < arr[left2])
{
tmp[cur++] = arr[left1++];
}
else
{
tmp[cur++] = arr[left2++];
}
}
while (left1 <= right1)
{
tmp[cur++] = arr[left1++];
}
while (left2 <= right2)
{
tmp[cur++] = arr[left2++];
}
memcpy(arr + i, tmp + i, (right2 - i + 1) * sizeof(int));
}
gap *= 2;
}
free(tmp);
}
快速排序
☝️☝️再次提醒本文是以升序的逻辑介绍的☝️☝️
快速排序是一个逐渐趋于有序的过程
void QuickSort(int *arr, int begin, int end)
{
if (begin >= end)
{
return;
}
int keyi = PartSort1(arr, begin, end);//使用不同的排序方法
// [begin, keyi-1] keyi [keyi+1, end]
QuickSort(arr, begin, keyi - 1);
QuickSort(arr, keyi + 1, end);
hoare版本
💃💃💃💃💃💃💃💃💃💃💃💃💃💃💃💃💃💃💃💃💃
hoare版本的思想就是通过使用两个指针在数组两侧寻找合适的值进行交换,左边找一个大于键值key的值,右边找一个小于key的值然后进行交换,最后两指针相遇位置就是key的正确位置,左侧都小于key值,右侧的值都大于key值,最后在keyi位置进行二分再次重复以上步骤,在逐渐有序的过程中最后有序。
此时就有一个问题需要我们思考为什么最后相遇的位置就是key值的正确位置呢?为什么右侧先走就能实现这个效果呢?
相遇时有两种情况,最后是右指针去和左指针相遇,或者最后是左指针去和右指针相遇。这两种情况都是相遇位置是一个小于key的值或者是相遇位置就是keyi的位置。
💚💚💚
右指针去和左指针相遇:这种情况也有两种可能,正常情况就是,在右指针走之前俩位置就已经交换过了,那左指针现在的位置就是一个小于key的值,与keyi位置的值进行交换使左右两侧得到合适的值。另一种情况就是右指针就没有找到小于key的值最后与左指针相遇,相遇位置就是keyi,就是key值的正确位置。
左指针去和右指针相遇:这种情况只有一种可能性,就是右指针已经找到了比key小的值,相遇后交换,得到我们想要的结果。
💚💚💚
假如说左侧先走,单从一种情况就可以看出它的不合理,左侧没有找到大于key的值最后和右指针相遇,右指针是一个什么值呢,无法保证,如果恰巧是一个小于key的值还正确,如果不是就出现错误。我们不禁感慨hoare真是一个大佬呀!
//hoare版本
int PartSort1(int* arr, int left, int right)
{
//int midi = GetMidi(arr, left, right);
//swap(&arr[left], &arr[midi]);
//三数取中优化排序
int keyi = left;
while (left < right)
{
//右边找小
while (left < right && arr[keyi] <= arr[right])
{
right--;
}
//左边找大
while (left < right && arr[keyi]>=arr[left])
{
left++;
}
//把大的扔到右边,把小的扔到左边
swap(&arr[left], &arr[right]);
}
swap(&arr[left], &arr[keyi]);
return left;//最后相遇的位置是键值arr[key]的正确位置
}
挖坑法
挖坑法的本质和hoare快排版本一样,使数据趋于有序,使最后一个坑的左侧小于坑里的值,右侧大于坑里的值。
//挖坑法
int PartSort2(int *arr, int left, int right)
{
//int midi = GetMidi(arr, left, right);
//swap(&arr[left], &arr[midi]);
//三数取中优化排序
int key = arr[left];
// 保存key值以后,左边形成第一个坑
int hole = left;
while (left < right)
{
//右边找小
while (left < right && key <= arr[right])
{
right--;
}
//填坑
arr[hole] = arr[right];
//掘坑
hole = right;
//左边找大
while (left < right && key>=arr[left])
{
left++;
}
//填坑
arr[hole] = arr[left];
//掘坑
hole = left;
}
//最后一个坑埋key,这个坑是key值的正确位置
arr[hole] = key;
return hole;
}
前后指针法
挖坑法的本质和hoare快排版本一样,使数据趋于有序,使最后prve位置,左侧小于prev位置的值,右侧大于prev位置的值。
//前后指针法
int PartSort3(int *arr, int left, int right)
{
//int midi = GetMidi(arr, left, right);
//swap(&arr[left], &arr[midi]);
//三数取中优化排序
int prev = left;
int cur = prev + 1;
int keyi = left;
while (cur <= right)
{
if (arr[cur] < arr[keyi] && ++prev != cur)
{
swap(&arr[cur], &arr[prev]);
}
cur++;
}
swap(&arr[keyi], &arr[prev]);
return prev;
}
快排逻辑优化
假设想如果对一个有序数组排序,我们每次都选最左侧数据还合理吗?在这种情况下,我们就需要对一个有n个数据元素的数组进行n次划分进行排序才能得到结果。
这时我们就需要对快排进行优化,优化选择键值的方式,不再选最左侧数据,而是在最左侧,最右侧,和中间位置选一个中位数当作键值,就对数组划分合理化了。
还有一种最极端的情况,尼玛数组数据全相同,在这情况下什么三数取中也完全没有效果,我们只能随机取keyi进行划分了。😬
//三数取中
int GetMidi(int* arr, int left, int right)
{
int mid = (left + right) / 2;
if (arr[mid] < arr[right])
{
if (arr[mid] > arr[left])
{
return mid;
}
else if (arr[right] < arr[left])
{
return right;
}
else {
return left;
}
}
else
{
if (arr[right] > arr[left])
{
return right;
}
else if (arr[mid] < arr[left])
{
return mid;
}
else
{
return left;
}
}
}
快速排序非递归实现
void QuickSortNonR(int* a, int begin, int end)
{
Stack st;
StackInit(&st);
StackPush(&st, end);
StackPush(&st, begin);
while (!StackEmpty(&st))
{
int left = StackTop(&st);
StackPop(&st);
int right = StackTop(&st);
StackPop(&st);
int keyi = PartSort1(a, left, right);
// [lefy,keyi-1] keyi [keyi+1, right]
if (keyi + 1 < right)
{
StackPush(&st, right);
StackPush(&st, keyi + 1);
}
if (left < keyi - 1)
{
StackPush(&st, keyi - 1);
StackPush(&st, left);
}
}
StackDestroy(&st);
}
快速排序非递归实现本质上是对递归的一种模拟通过栈控制排序的划分区间,先把要排序区间的边界存放在栈中,对此区间完成排序后,再对此区间进行划分,依次对两区间的左右边界入栈。栈空则完成排序。
🌈🌈🌈🌈本文到此结束,希望对读者有所帮助🌈🌈🌈🌈
人生亦可然,亦可腐败,我愿燃烧,耗尽所有的光芒。