目录
常见的排序算法
常见的排序算法有插入排序(直接插入排序,希尔排序),选择排序(选择排序,堆排序),交换排序(冒泡排序,快速排序),归并排序,以及计数排序。本文将会详细介绍上述排序(按照升序的形式)以及它们的实现形式(递归与非递归)。
一、插入排序
1.直接插入排序
思想:
直接插入排序是从需要排序的数组的一个元素开始,每次开始比较的数记作(x),将它与之前(直到第一个元素)依次比较,如果这个数比本次x大,那么便向后移动一个距离。我们从第一个元素开始把它作为x的,便保证了数组的有序,当我们找到比这个数小的数的时候便可以停下,将停下的这个数后面的位置放入我们x。这样依次遍历,直到到数组的最后一个元素,便可以完成数组的排序。
时间复杂度与空间复杂度:
我们可以看出,在最好的情况下,即原数组为升序的情况下,插入排序只需要遍历一次数组,最好的时间复杂度为O(N)。而最坏的情况里便是数组为逆序,则每个数都要与前面的数进行交换,这样最坏的时间复杂度就是O(N^2)。所以插入排序的时间复杂度在这两者之间。
我们并没有开辟额外的空间,所以空间复杂的为O(1)。
代码的实现:
为了保证代码的实现,我们在这里可以从第一个数开始,把它的后一个数作为x,依次与前面的数进行比较。
void InsertSort(int* a, int n)
{
assert(a);
for (int i = 0; i < n - 1;i++)
{
int end = i;
int x = a[end + 1];
while (end >= 0)
{
if (a[end]>x)
{
a[end + 1] = a[end];
end--;
}
else
{
break;
}
}
a[end + 1] = x;
}
}
2.希尔排序
思想:
希尔排序算法是对直接插入排序算法的优化。当时他想如果让这个数组接近有序,便可以提升这个算法的效率,于是他想出了一个方法。那便是定义一段步长(我们记作gap),让数组中每一组间隔gap的数字有序,而这个gap的定义可以由大到小依次变化,直到为1便成为了直接插入排序。而此时数组接近有序,直接插入排序的算法便可以得到优化。
代码的实现:
我们在取gap的时候,可以如给出的代码这样取。这样取可以每次减少gap的值,而+1保证了gap的最后值为1。也可以每次都gap=gap/2。
而我们的每次从0开始,让每个数的后一个数(x)与前面每一个相差步长为gap的数进行比较。这样便可一次遍历使数组更接近有序。
void ShellSort(int* a, int n)
{
int gap = n;
while(gap>1)
{
gap = gap / 3 + 1;
for (int i = 0; i < n - gap; i += 1)
{
int end = i;
int x = a[end + gap];
while (end >= 0)
{
if (a[end] > x)
{
a[end + gap] = a[end];
end -= gap;
}
else
{
break;
}
}
a[end + gap] = x;
}
}
}
二、选择排序
1.选择排序
思想:
选择排序是每次选出最大值或者最小值。也可以同时选出,这里我们给出的算法是同时选出最大值与最小值,把它们分别放到首尾位置,再从除去最大值最小值的数组中再次按照上述方法进行。直到完成排序。
代码的实现:
void Swap(int* num1, int* num2)
{
int tem = *num2;
*num2 = *num1;
*num1 = tem;
}
void SelectSort(int* a, int n)
{
int mini;
int maxi;
int begin = 0;
int end = n - 1;
while (begin < end)
{
mini = begin;
maxi = end;
for (int i = begin; i <= end; i++)
{
if (a[i] < a[mini])
{
mini = i;
}
if (a[i] > a[maxi])
{
maxi = i;
}
}
Swap(&a[begin], &a[mini]);
if (maxi == begin)
{
maxi = mini;
}
Swap(&a[end], &a[maxi]);
begin++;
end--;
}
}
2.堆排序
思想:
堆排序是利用数据结构的堆来进行排序,它实际上控制的物理结构是一个一维数组,而我们要把堆给想象成完全二叉树进行控制。同时堆排序的时间复杂度为O(N)是一种效率极高的排序方法。同时我们在需要排序的数组进行操作即可,不需要向内存申请额外的空间,所以空间复杂度为O(1)。
代码的实现:
我们只需要利用一个向下调整算法即可完成堆排序。
如果我们需要排升序,把原数组调整为大堆,再利用堆结构的pop思想即可完成操作。
堆结构的pop思想是把数组中的第一个元素与最后一个元素进行交换,删除掉最后一个元素,在利用向下调整把删除掉元素后的数组进行重新排序。
只有有孩子的根节点才可以进行向下调整。我们找到最后一个有孩子的父亲节点,把它和之前的节点依次进行向下调整。在完全二叉树中,度为0的节点比度为2的节点要多一个,我们利用这一个性质来计算最后一个父亲节点的位置,因为只有节点个数为偶数时才会出现度为1的节点,所以我们-1后不会影响结果。
void AdjustDown(int* a, int father,int n)
{
int child = father * 2 + 1;
while (child < n)
{
if (a[child + 1] > a[child] && child + 1 < n)
{
child++;
}
if (a[child] > a[father])
{
Swap(&a[child], &a[father]);
father = child;
child = father * 2 + 1;
}
else
{
break;
}
}
}
void HeapSort(int* a, int n)
{
for (int i = n-1-1; i >=0; i--)//这里的i为最后一个父亲节点的位置
{
AdjustDown(a, i, n);
}
for (int i = 0; i < n; i++)
{
Swap(&a[0], &a[n - 1 - i]);
AdjustDown(a, 0, n -1- i);
}
}
三、交换排序
1.冒泡排序
思想:
步骤一:遍历,把最大的数交换到数组的最后。
步骤二:将数组的大小缩小一。
步骤三:重复步骤一、二。
代码的实现:
void BubbleSort(int* a, int n)
{
for (int i = 0; i < n - 1; i++)
{
int flag = 1;
for (int j = 0; j < n - 1 - i; j++)
{
if (a[j] > a[ j+ 1])
{
flag = 0;
Swap(&a[j], &a[j + 1]);
}
}
if (flag)
break;
}
}
2.快速排序
思想:
快速排序是由霍尔提出的,霍尔提出的方法是最初的快速排序方法,后来又衍生出了挖坑法以及前后指针法等。本篇博客将会为读者依次讲解霍尔法,挖坑法,前后指针法。
我们先给一个无序的一维数组。为升序的思想。
最初的快排方法(霍尔法):
我们先进行快速排序中单趟排序的讲解。快速排序的思想是先找到一个关键字的值,可以为下标也可以为具体的数,我们通常会选择数组的最左边或最右边作为的关键字,在选择关键字后,假设我们选择左边为关键字。我们需要首先从最右边开始向左寻找,当我们找到一个比关键字小的位置时停下,然后再从关键字的位置开始向右边开始寻找,找到一个比关键字大的位置停下,然后交换它们两个。接下来再按照刚才从右边先开始的方法进行寻找,直到左右相遇。相遇的位置一定是比关键字小,所以在此交换关键字和最后停留的位置,完成单趟排序。
挖坑法:
步骤如下:
我们把坑定在最左边,把这个值进行保留。
1.从数组最右边开始出发,找到一个比坑小的数,把它放到坑里,让这个数的位置成为新的坑。
2.从数组最左边开始出发,找到一个比坑大的数,把它放到坑里,让这个数的位置成为新的坑。
重复步骤1,2直到left,right相遇,把最后一个坑填入一开始保留的值。完成单趟排序,再次按照最初的快排方法的分治思想,从左右两个部分进行挖坑,直到把数组分割成每个数为一个单元。
前后指针法:
步骤如下,我们假设从左边开始定义关键值。
我们需要一个cur=left+1,一个prev=left,让cur向右找比关键值小的值,找到后与++prev交换。直到cur走出数组。最后返回prev的值。这里需要注意两个地方:一是在我们交换完后cur需要++,否则会进入死循环。再一个是我们要确保cur<=right,防止数组越界。这样也可以完成单趟排序的操作。
完成单趟排序后,我们会发现关键字的左边都比关键字要小,而关键字的右边都比关键字要大,所以关键字来到了一个合适的位置。接下来通过分治的思想,把相遇点的左右两个部分分开,再按照这个方法,最终缩小到只有一个数的时候停下,那么快速排序就完成了。
快排中的一些问题:
1.为什么需要从右边开始寻找比关键字小的数:
如果从左边先开始,那么停下来的数会比关键字要大,这时无法完成我们单趟排序的需求。
2.快速排序的时间复杂度。
最坏情况:
当我们的关键字每次都是数组中最小数时,我们需要分成N层才可以把每个数分成一个单元,而每一层需要遍历这一层数组的个数-1,时间复杂度也就到达了O(N^2)。
理想状况:
每次选取的关键字位于数组中间,每层分时对半分,可以想象成二叉树,共会分成log2N层,时间复杂度也就到了O(N*log2N)。
所以快速排序的时间复杂度我们一般取O(N*logN)。
3.如何避免最坏情况。
我们可以利用一个getmid函数,来获取left,right,mid的中间值作为我们的关键字,这样就会有效避免最坏情况的发生。
4.小区间优化。
快速排序的算法比较像二叉树,越往下需要的次数越多,因此我们可以进行小区间优化,当区间很小的时候,我们直接运用插入排序即可。这样就会对快排算法进行优化
代码的实现:
这里我们先给出递归实现的方法。
int Partion1(int* a, int left, int right)//霍尔法
{
int keyi = left;
while (left < right)
{
while (left < right && a[right] >= a[keyi])
right--;
while (left < right && a[left] <= a[keyi])
left++;
Swap(&a[left], &a[right]);
}
Swap(&a[keyi], &a[left]);
return left;
}
int Partion2(int* a, int left, int right)//挖坑法
{
int pivot = left;
int key = a[left];
while (left < right)
{
while (left < right && a[right] >= key)
right--;
a[pivot] = a[right];
pivot = right;
while (left < right && a[left] <= key)
left++;
a[pivot] = a[left];
pivot = left;
}
a[pivot] = key;
return pivot;
}
int GetMidNum(int* a, int left, int right)//获取三个数的中间值
//避免最坏情况
{
int mid = (left + right) / 2;
if (a[left] < a[mid])
{
if (a[mid] < a[right])
return mid;
else if (a[left] < a[right])
return right;
else
return left;
}
else
{
if (a[left] < a[right])
return left;
else if (a[mid] < a[right])
return right;
else
return mid;
}
}
int Partion3(int* a, int left, int right)//前后指针法
{
int cur = left+1;
int keyi = left;
int prev = left;
while (cur <= right)
{
if (a[cur] < a[keyi])
{
Swap(&a[++prev], &a[cur]);
}
cur++;
}
Swap(&a[keyi], &a[prev]);
return prev;
}
void QuickSort(int* a, int left,int right)
{
if (left >= right)
return;
int TheMid = GetMidNum(a, left, right);
Swap(&a[TheMid], &a[left]);
int keyi = Partion1(a, left, right);
QuickSort(a, left, keyi - 1);
QuickSort(a, keyi+1, right);
}
非递归实现的方法:
我们可以利用栈来完成快排的非递归算法。首先我们把数组的左右区间下标(这里我们先放入的左下标,再放入的右下标,那么出栈时便是先出右下表,再出左下标)放入到栈中,每次取出一组下标,让每一组中选出一个keyi值来,如果keyi的左右区间是合法的,那么再把左右区间放入到栈中,如果不合法,则不把这个区间放入栈中。直到栈空,非递归实现快排的算法即可实现。
这里我们运用到了栈,可以参考博主之前发的实现栈的文章。
void QuickSortNonR(int* a, int left, int right)
{
ST* theStack = (ST*)malloc(sizeof(ST));
StackInit(theStack);
StackPush(theStack, left);
StackPush(theStack, right);
while (!StackEmpty(theStack))
{
int end = stackTop(theStack);
StackPop(theStack);
int begin = stackTop(theStack);
StackPop(theStack);
int keyi = Partion3(a, begin, end);
if (keyi + 1 < end)
{
StackPush(theStack, keyi + 1);
StackPush(theStack, right);
}
if (begin < keyi - 1)
{
StackPush(theStack, left);
StackPush(theStack, keyi - 1);
}
}
StackDestory(theStack);
}
四、归并排序
思想:
我们先把数组进行拆分成多个小区间,让小区间有序。再把多个小区间通过归并合并成一个大区间,让小区间上的上一层大区间有序。直到大区间为整个数组,即可完成归并排序。
代码的实现:
递归实现:
这里我们运用一个子函数来实现递归。
void _MergeSort(int* a, int left, int right,int*tem)
{
if (left >= right)
{
return;
}
int mid = (left + right) / 2;
_MergeSort(a, left, mid, tem);
_MergeSort(a, mid+1, right, tem);
int begin1 = left;
int end1 = mid;
int begin2 = mid + 1;
int end2 = right;
int i = left;
while (begin1 <= end1 && begin2 <= end2)
{
if (a[begin1] <= a[begin2])
{
tem[i++] = a[begin1++];
}
else
{
tem[i++] = a[begin2++];
}
}
while (begin1 <= end1)
{
tem[i++] = a[begin1++];
}
while (begin2 <= end2)
{
tem[i++] = a[begin2++];
}
for (int j = left; j <= right; j++)
{
a[j] = tem[j];
}
}
void MergeSort(int* a, int left, int right)
{
int* tem = (int*)malloc(sizeof(int) * (right - left+1));
if (tem == NULL)
{
printf("malloc fail\n");
exit(-1);
}
_MergeSort(a, left, right, tem);
}
非递归实现 :
这里的思想与递归是相似的。我们运用一个循环来实现非递归。我们首先把步长定为1,先让区间大小为1的两个区间归并到tem数组,再把tem数组中有的值考回到原数组。再扩大gap的倍数来进一步增大归并的区间,直到区间覆盖原数组。
边界情况
在这里,我们的left1永远不会越界,所以也就会产生三种越界情况,即right1越界,left2越界,right2越界,当right1越界的时候,我们不需要再往下进行了,因为我们是每执行一次便把tem数组考回原数组。所以当right1越界我们就直接跳出。并且我们是从区间为1开始的。当right2越界并且left2未越界时,我们便需要进行归并,这是我们修正right2即可,把right2修正为right即可。
另一种思路便每次都把tem数组中的所有值都考回去,这样的话需要对三种越界情况都进行修正,这里没有给出相关代码,感兴趣的读者可以自己写一下。
void MergeSortNonR(int* a, int left, int right)
{
int n = right - left + 1;
int* tem = (int*)malloc(sizeof(int) * n);
if (tem == NULL)
{
printf("malloc fail\n");
exit(-1);
}
int gap = 1;
while (gap < n)
{
for (int i = 0; i <= right; i += 2 * gap)
{
int left1 = i;
int right1 = i + gap - 1;
int left2 = i + gap;
int right2 = i + 2 * gap - 1;
if (right1 >= right)
{
continue;
}
if (right2 >= right && left2 <= right)
{
right2 = right;
}
int j = i;
while (left1 <= right1 && left2 <= right2)
{
if (a[left1] <= a[left2])
{
tem[j++] = a[left1++];
}
else
{
tem[j++] = a[left2++];
}
}
while (left1 <= right1)
{
tem[j++] = a[left1++];
}
while (left2 <= right2)
{
tem[j++] = a[left2++];
}
for (j = 0; j <= right2; j++)
{
a[j] = tem[j];
}
}
gap *= 2;
}
free(tem);
tem = NULL;
}
五、计数排序
思想:
计数排序的思想类似于哈希表。适合运用在极值范围比较小的区间,我们利用相对映射的关系,把原数组中拥有的数的个数统计到一个count数组中,再根据count数组与原数组的映射关系,把正确的值再放回到原数组。
代码的实现:
void CountSort(int* a, int n)
{
int max = a[0];
int min = a[0];
for (int i = 1; i < n; i++)
{
if (a[i] > max)
{
max = a[i];
}
if (a[i] < min)
{
min = a[i];
}
}
int range = max - min + 1;
int* count = (int*)malloc(sizeof(int) * range);
if (count == NULL)
{
printf("malloc fail\n");
exit(-1);
}
memset(count, 0, sizeof(int) * range);
for (int i = 0; i < n; i++)
{
count[a[i] - min]++;
}
int j = 0;
for (int i = 0; i < range; i++)
{
while (count[i]--)
{
a[j++] = i + min;
}
}
}