C语言实现常见八大排序万字详解
Posted SimplexXx0
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C语言实现常见八大排序万字详解相关的知识,希望对你有一定的参考价值。
八种排序的动图展示讲解
插入排序
插入排序是指在待排序的元素中,假设前面n-1(其中n>=2)个数已经是排好顺序的,现将第n个数插到前面已经排好的序列中,然后找到合适自己的位置,使得插入第n个数的这个序列也是排好顺序的。 按照此法对所有元素进行插入,直到整个序列排为有序的过程,称为插入排序 。类似打扑克牌插入
1.思想:
假设一组数有n个元素,前n-1个已有序,记第n-1个位置为end,
用一个临时变量x记录第n个元素的值,x依次从end开始依次往前比,比x大的往后挪,找到比x小的停下,把x插入到它的后面。类似于打扑克牌。
2.图解:
单趟排序
整组排序
按照单趟排序方法对整组进行多趟排序
3.代码实现
void InsertSort(int*a, int n)
for(int i = 0; i < n-1;i++)
int end = i;
int tem = a[end+1];
while(end > 0)
if(a[end] > tem)//比tem大往后挪
a[end+1] = a[end];
end--;
else//比tem小停止
break;
a[end+1] = tem;//在后面把tem插入
4.性能分析
- 时间复杂度:O(N^2)
- 空间复杂度: O(1)
- 稳定性:稳定
希尔排序
希尔排序也是一种插入排序,是直接插入排序算法的一种更高效的改进版本,又称“缩小增量排序”。希尔排序是非稳定排序算法。
1.思想:
把一个数组分成gap组,gap也是每组相邻两个元素之间的间距。对每一组进行直接插入排序(又称预排序),使整个数组接近有序。完成预排序后,使gap=1,也就是在最后对整个数组进行一次单趟排序,数组有序,完成排序。
可以进行多次预排序,使数组更加接近有序(具体做法:完成一趟预排序后,改变gap再次进行预排序)
gap越小,排的越慢,数组越接近有序。
2.图解
3.代码实现
void ShellSort(int* a, int n)
for (int gap = n / 2; gap > 0; gap /= 2)//改变gap,进行多次预排序
for (int i = 0; i < n - gap; i++)//每次对一个数在其本组上进行单趟插入排序,
//i++相当于全部的组轮序着排,并不是一组先排完后再排另一组
int end = i;//这里开始和插入排序一模一样,只是把间距变成了gap
int tmp = a[end + gap];
while (end >= 0)
if (a[end] > tmp)
a[end + gap] = a[end];
end -= gap;
else
break;
a[end + gap] = tmp;
4.性能分析
- 时间复杂度:O(N*logN)
- 空间复杂度: O(1)
- 稳定性:不稳定
选择排序
选择排序是一种简单直观的排序算法。它的工作原理是:第一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,然后再从剩余的未排序元素中寻找到最小(大)元素,然后放到已排序的序列的末尾。
1.思想:
(一次找一个数)遍历找到最小的数放到最边上
(一次找两个数)遍历数组,找出最大的数和最小的数,分别放到最两边,再对中间的无序数组再选最大最小,放到中间无序数列的两头
2.图解
一次选一个数
一次选两个数
选出小的放左边,大的放右边,图像应该比较好想象。
3.代码实现
void SelectSort(int* a, int n)
int begin = 0; int end = n - 1;
while (begin < end)
int min = begin; int max = end;
for (int i = begin; i <= end; i++)
if (a[i] < a[min])
min = i; //mini记录查找过程中最小值的位置下标
if (a[i] > a[max])
max = i; //maxi记录查找过程中最大值的位置下标
Swap(&a[min], &a[begin]); //把找到的最小值放到数组最左边
if (max = begin)
min = max;
Swap(&a[max], &a[end]); //把找到的最大值放到数组最右边
//控制范围,对剩下中间的数再进行选择、排序
++begin;
--end;
代码注解
代码中两个Swap函数之间的if条件判断,是为了防止出现一次选两个数的特殊情况而做的修正。如果max的位置和begin位置相同,那min和begin交换后,max位置的原始值已经变了,如果不修正就进行max和end交换会发生错误。
Swap(&a[min], &a[begin]);
if (max = begin)
min = max;
Swap(&a[max], &a[end]);
堆排序
1.思想:
需要用到二叉树大小堆的概念。堆的逻辑结构是一棵完全二叉树,物理结构是数组。给我们一个数组,要排升序,我们就要把数组建成大堆;排降序,就建小堆。这里我们用向下调整AdjustDown()来建一个大堆(父节点比任何一个子节点大叫大堆)
完成一棵树(或子树)的堆排序操作图解
向下调整的代码实现
void Swap(int* x,int* y)//交换函数
int tem = *x;
*x = *y;
*y = tem;
void AdjustDown(int*a,int n,int root)
int child = root*2 + 1;
int parent = root;
for(child < n)
//找到两个孩子中大的那一个
if(a[child+1]<n && a[child+1]>a[child])
child++;
if(a[child]>a[parent])
Swap(&a[child],&a[parent]);
parent = child;//交换完后再往下对比
child = parent*2 - 1;
对于要调一棵树,需要先保证把它的子树全部调成堆。所以我们可以从最后一棵子树的父亲开始调整,调完一棵子树再从此父节点依次往前一个结点调,最后完成整棵树的堆排序。
调完堆后就可以对堆里的数据进行排序了
思路:把堆顶数据和最后一棵子树调换位置,对除了尾结点的前n-1棵树进行调堆
void HeapSort(int* a, int n)//堆排序
//排升序,先把数组建成大堆
int parent = (n - 1 - 1) / 2; //建大堆时,从最后一个孩子的父亲开始调
for (int i = parent; i >= 0; i--)
AdjustDown(a, n, i);
for (int end=n-1;end>=0;end--)
Swap(&a[0], &a[end]);//堆顶和最后一个孩子交换,前n-1个再调整成大堆
AdjustDown(a, end, 0);
冒泡排序
这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端(升序或降序排列),就如同碳酸饮料中二氧化碳的气泡最终会上浮到顶端一样,故名“冒泡排序”。
1.思想
依次比较两个相邻的元素,前一个比后一个大则交换,否则继续比较下一对(升序),一趟完之后最大的数就被排在了最后面。再对前面的数继续排序,完成整组排序。
2.图解
单趟冒泡排序
3.代码实现
void BbubleSort(int* a, int n)
assert(a);
int end = n;
while (end > 0)
int exchange = 0;
for (int i = 0; i < end - 1; i++)
if (a[i] > a[i + 1]) //当判断条件为if(a[i-1]>a[i]),循环条件为for(int i=1;i<end;i++)
exchange = 1;//判断是否交换过,如果一趟内未发生交换,则数组已有序,不用再排
Swap(&a[i], &a[i + 1]);
end--;
if (exchange == 0)//如果一趟内没有发生交换,则表明数组已有序,跳出循环
break;
快速排序
快速排序是Hoare于1962年提出的一种二叉树结构的交换排序方法
1.思想
这里是引用快速排序算法通过多次比较和交换来实现排序,其排序流程如下:
- 任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序序列分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后左右子序列重复该过程划分各自的子序列,直到所有元素都排列在相应位置上为止。
- 重复上述过程,可以看出,这是一个递归定义,类似二叉树的前序遍历。通过递归将左侧部分排好序后,再递归排好右侧部分的顺序。当左、右两个部分各数据排序完成后,整个数组的排序也就完成了。采用了分而治之的思想(分成小区间,让各自区间各自排序)
三数取中法:如何有效地选择基准值做key?
一般我们选最左/右边或随机选择的值来做key,不免会发生选到的就是最大值或最小值的情况,这样排序的效率就会很低。我们拿最左/右边的值和中间的值,三个值比较,取中间的不大不小的值来做key就可以解决这个问题。
int MidIndex(int* a,int left, int right)
//int mid = left + right/2;
int mid = left + ((right - left) >> 1);//二进制往右移一位相当于除二,移两位相当于除四
if (a[left] < a[mid])
if (a[mid] < a[right])
return mid;
else
return a[left] < a[right] ? right : left;
else //(a[left] > a[mid])
if (a[mid] > a[right])
return mid;
else
return a[right] > a[left] ? left : right;
通过基准值划分为左右子区间的常见方法
1.Hoare版本(左右指针法)
思想:
一般选最左/右边的值作为基准值key,
- 左指针left从头开始找比key小的值,找到停下;
- 然后右指针right从尾开始往前走,找到比key大的值,找到停下;
- 交换此时left 和 right 的值。交换完重复步骤继续找、交换。
- 当最后left 和 right 相遇时,把相遇位置的值和key交换。此时key的左边全部为比key小的,右边全部为比key大的。完成区间划分
使用原则
(因为最后一步要把相遇位和key交换,这样能保证key的左边比key小,key的右边比key大)
选最左边的做基准值,右指针先走→(右边先走找小于key的值,一直找如果找不到,走到 left 相遇停下,left位是比key小的,保证了相遇位比key值小)
选最右边的做基准值,左指针先走→(同上理,能保证相遇位比key大)
代码实现
int partition1(int* a, int left, int right)//单趟排序
int mid = MidIndex(a, left, right);//三数取中
Swap(&a[left], &a[mid]);//把中间数位置换为left
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[left], &a[keyi]);
return left;
特殊场景
像这两种情况left 和 right 找不到相应的值就会一直走,导致越界,为了防止越界,要在while循环里加判断条件 left < right
递归程序缺陷:递归深度太深会导致栈溢出
2.挖坑法
挖坑法是左右指针法的一种变形
思想:
- 将第一个值作为基准值保存到临时变量key里,形成一个坑位pivot
- 假设选左边做坑,那右边先走,找到小,放进坑里,右就变成了新的坑
- 然后左边走,找到大,放到坑里,左变成新的坑。然后右边走,重复步骤。
- 相遇停下的时候把key值填到pivot坑中
图解
代码实现
int partition2(int* a, int left,int right)
int mid = MidIndex(a, left, right);
Swap(&a[left], &a[mid]);
int key = a[left];
int pivot = 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;
3.前后指针法
思想:
- 初始时,prev指针指向序列开头,cur指针指向prev的后一个位置,取基准值key
- cur找小,找到小后,++prev往后一步,把 cur 和 prev 交换(相当于找到小的往前放)
- cur 走完整个序列,把 prev 和 key 交换。
图解:
代码实现
int partition3(int* a, int left, int right)
int mid = MidIndex(a, left, right);
Swap(&a[left], &a[mid]);
int key = left;
int prev = left;
int cur = left + 1;
while (cur <= right)
if (a[cur] < a[key] && ++prev != cur)
//如果一开始cur就比key小,prev往后走就赶上了cur,交换后相当于没交换,所以如果prev的下一个是cur的让它们两个一起往后走一步就行了
Swap(&a[cur], &a[prev]);
cur++;
Swap(&a[prev], &a[key]);
return prev;
递归法小区间优化
递归调用层次越深,区间划分越多,递归调用的次数就越多,效率就会降低。我们可以考虑后面的几层用其他排序方法排序,可以大大减少调用递归的次数,防止栈溢出,提升排序效率。
代码汇总
//三数取中
int MidIndex(int* a,int left, int right)
//int mid = left + right/2;
int mid = left + ((right - left) >> 1);//二进制往右移一位相当于除二,移两位相当于除四
if (a[left] < a[mid])
if (a[mid] < a[right])
return mid;
else
return a[left] < a[right] ? right : left;
else //(a[left] > a[mid])
if (a[mid] > a[right])
return mid;
else
return a[right] > a[left] ? left : right;
//区间划分1.hoare左右指针
int partition1(int* a, int left, int right)//单趟排序
int mid = MidIndex(a, left, right);//三数取中
Swap(&a[left], &a[mid]);//把中间数位置换为left
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[left], &a[keyi]);
return left;
//区间划分2:挖坑法。
// 思想:选left或right做坑,这里选left做坑pivot,key记录当前值,right先走,right找到比key小的数停下,
// 把right填到坑里,然后right做坑,left走,找到大于key的数,填进坑,left做坑,right走
int partition2(int* a, int left,int right)
int mid = MidIndex(a, left, right);
Swap(&a[left], &a[mid]);
int key = a[left];
int pivot = left;
while (left < right)
while(left<right && a[right] > key)
right--;
a八大排序算法C语言过程图解+代码实现(插入,希尔,选择,堆排,冒泡,快排,归并,计数)