八大排序
一、内部排序
1.1 插入排序
1.1.1 直接插入排序
直接插入排序的核心思想就是:将数组中的所有元素依次跟前面已经排好的元素相比较,如果选择的元素比已排序的元素小,则交换,直到全部元素都比较过。
因此,从上面的描述中我们可以发现,直接插入排序可以用两个循环完成:
- 第一层循环:遍历待比较的所有数组元素
- 第二层循环:将本轮选择的元素(selected)与已经排好序的元素(ordered)相比较。
如果:selected > ordered,那么将二者交换
假设排序顺序从左至右,具体步骤如下:
-
列表第一个元素和前面元素比较,如果小于前面元素(其实不存在),则交换位置。(这步其实可以没有)
-
列表第二个元素和前面元素(第一个元素)比较,如果小于前面元素,则交换位置。
-
列表第三个元素和前面元素(第二个元素)比较,如果小于前面元素,则交换位置。如果和前面元素交换了位置,现在在第二个位置上,则接着继续和前面元素比较(第一个元素),如果小于前面元素,接着再次交换位置,然后再次重复比较过程…
…继续重复以上过程,直到最后一个元素完成比较
比较移动过程中,如果元素不需要移动意味着该元素排序完毕。
#include <stdio.h>
#include <string.h> //能够使用strlen()函数
char a[10] = { 8,4,15,2,5,10,14,6,8,18 };
int main(void)
{
int temp = 0;
int len = strlen(a); //取得数组长度
for (int i = 1; i < len; i++) //遍历待比较的所有数组元素
{
//将本轮元素与前面已经排好序的元素进行比较,如果前面的元素大于后面的元素,则一直交换,否则结束此次for循环
for (int j = i-1; j >= 0 && a[j] > a[j + 1]; j--)
{
temp = a[j];
a[j] = a[j + 1];
a[j + 1] = temp;
}
}
for (int i = 0; i < 10; i++) //打印数组
{
printf("%d ", a[i]);
}
getchar();//执行执行完毕后不立即返回代码窗口,等待回车后再返回,便于查看运行结果。
return 0;
}
1.1.2 希尔排序
希尔排序的算法思想:将待排序数组按照步长gap进行分组,然后将每组的元素利用直接插入排序的方法进行排序;每次将gap折半减小,循环上述操作;当gap=1时,利用直接插入,完成排序。
同样的:从上面的描述中我们可以发现:希尔排序的总体实现应该由三个循环完成:
- 第一层循环:将gap依次折半,对序列进行分组,直到gap=1
- 第二、三层循环:也即直接插入排序所需要的两次循环。具体描述见上。
#include <stdio.h>
#include <string.h> //能够使用strlen()函数
char a[10] = { 8,4,15,2,5,10,14,6,8,18 };
int main(void)
{
int len = strlen(a); //取得数组长度
int increment = len;
int temp = 0;
int j=0;
while (increment > 1)
{
increment = increment / 3 + 1; //增量的取法之一:除三向下取整+1
for (int i = increment; i < len; i++) //进行(len - increment)轮
{
if (a[i- increment] > a[i]) //前面的数和后面的数比较比较
{
temp = a[i]; //如果前面的数比较大,则把后面的数存入temp中
j = i - increment; //j代表前面的数
while (j >= 0 && a[j] > temp)
{
//如果前面的数比后面的大,则后面的数等于前面的数
a[j + increment] = a[j];
//如果出现a[1]和a[5]比完,a[5]与a[9]比,如果a[5]>a[9],则a[1]再和a[9]比
j = j - increment;
}
//如果a[1]>a[9],则a[5]不变,a[1]与a[9]互换;如果a[1]<a[9]&&a[5]>a[9],则a[5]与a[9]互换
a[j + increment] = temp;
}
}
}
for (int i = 0; i < 10; i++) //打印数组
{
printf("%d ", a[i]);
}
getchar();//执行执行完毕后不立即返回代码窗口,等待回车后再返回,便于查看运行结果。
return 0;
}
1.2 选择排序
1.2.1 简单选择排序
简单选择排序的基本思想:比较+交换
- 从待排序序列中,找到关键字最小的元素;
- 如果最小元素不是待排序序列的第一个元素,将其和第一个元素互换;
- 从余下的 N - 1 个元素中,找出关键字最小的元素,重复(1)、(2)步,直到排序结束。
因此我们可以发现,简单选择排序也是通过两层循环实现。
第一层循环:依次遍历序列当中的每一个元素
第二层循环:将遍历得到的当前元素依次与余下的元素进行比较,符合最小元素的条件,则交换。
#include <stdio.h>
#include <string.h> //能够使用strlen()函数
char a[10] = { 8,4,15,2,5,10,14,6,8,18 };
int main(void)
{
int dat = 0; //用来保存每次遍历最小值的数组下标
int temp = 0;
int len = strlen(a); //取得数组长度
for (int i = 0; i < len-1; i++) //遍历整个数组(最后一个不用)
{
dat = i;
for (int j = i + 1;j < len; j++) //从i以后遍历数组与i比较
{
if (a[j] < a[dat]) //找出最小的值,把该值的数组下标给dat
{
dat = j;
}
}
temp = a[i]; //dat的值与i的值的位置进行交换,放到已排序列表后面
a[i] = a[dat];
a[dat] = temp;
}
for (int i = 0; i < 10; i++) //打印数组
{
printf("%d ", a[i]);
}
getchar();//执行执行完毕后不立即返回代码窗口,等待回车后再返回,便于查看运行结果。
return 0;
}
1.2.2 堆排序
堆排序的实现:
- 堆排序是利用堆的性质进行的一种选择排序。
其基本思想为(大顶堆):
-
假设一个初始待排序的长度为n;
-
将初始待排序关键字序列(R0,R1,R2,…,Rn-1)构建成大顶堆,此堆为初始的无序区;
-
将堆顶元素R[0]与最后一个元素R[n-1]交换,此时得到新的无序区(R0,R2,…,Rn-2)和新的有序区(Rn-1),且满足R[1,2,…,n-2]<=R[n-1];
-
由于交换后新的堆顶R[0]可能违反堆的性质,因此需要对当前无序区(R0,R1,…,Rn-2)调整为新堆,然后再次将R[0]与无序区最后一个元素交换,得到新的无序区(R0,R1,R2,…,Rn-3)和新的有序区(Rn-2,Rn-1)。
-
不断重复此过程直到有序区的元素个数为n-2,则整个排序过程完成。
#include <stdio.h>
#include <string.h> //能够使用strlen()函数
//#include <stdlib.h>
void HeapAdjust(char array[], int i, int len)
{
int MaxChild; //用来存放最大子节点
int Parent; //用来存放父节点的值
int LeftChild = 2 * i + 1; //假设父节点为i,则其左子节点为2i+1
int RightChild = 2 * i + 1; //假设父节点为i, 则右子节点为2i + 2
for (Parent = array[i]; 2 * i + 1 < len; i = MaxChild) //每次将最大子节点作为父节点向下判断
{
LeftChild = 2 * i + 1;
RightChild = 2 * i + 2;
MaxChild = LeftChild; //先假设左节点为最大值
if (MaxChild != len- 1 && array[RightChild] > array[LeftChild]) //先判断该节点有没有超过超度,再判断左右子节点哪个大, 找到值最大的孩子结点
{
MaxChild = RightChild; //如果右节点大,则进来,把MaxChild变为右子节点
}
if (Parent < array[MaxChild]) // 如果较大的子结点大于父结点那么把较大的子结点往上移动,替换它的父结点
{
array[i] = array[MaxChild];
}
else // 否则退出循环
{
break;
}
}
array[i] = Parent; // 最后把原本的父节点放到合适的位置
}
void HeapSort(char array[], int length)
{
//构建堆的过程
for (int i = (length / 2 - 1); i >= 0; i--) // 调整序列的前半部分元素,(即每个有孩子的节点)调整完之后是一个大顶堆,第一个元素是序列的最大的元素,从下往上判断
{
HeapAdjust(array, i, length);
}
for (int i = length - 1; i > 0; i--) //通过不断地筛选出最大值,同时不断地进行筛选剩余元素
{
int temp;
temp = array[i]; // 把第一个元素和当前的最后一个元素交换,保证当前的最后一个位置的元素都是在现在的这个序列之中最大的
array[i] = array[0];
array[0] = temp; //array[i]为最大值,arrat[i-1]为第二大,arrat[i-2]为第三大,以此类推
HeapAdjust(array, 0, i); // 不断缩小调整heap的范围,每一次调整完毕保证第一个元素是当前序列的最大值
}
}
int main()
{
int len;
char arr_test[17] = {15, 8, 12, 4, 2, 3, 5, 1, 6, 9, 0, 7, 10, 11, 14, 15, 1 }; //测试数据
len = sizeof(arr_test); //计算数组长度
HeapSort(arr_test, len); //堆排序算法
for (int i = 0; i < len; i++) //将排序好的数据打印出来
{
printf("%d ", arr_test[i]);
}
getchar(); //执行执行完毕后不立即返回代码窗口,等待回车后再返回,便于查看运行结果。
return 0;
}
1.3 交换排序
1.3.1 冒泡排序
冒泡排序是比较基础的排序算法之一,其思想是相邻的元素两两比较,较大的数下沉,较小的数冒起来,这样一趟比较下来,最大(小)值就会排列在一端。整个过程如同气泡冒起,因此被称作冒泡排序。
冒泡排序思路比较简单:
- 将序列当中的左右元素,依次比较,保证右边的元素始终大于左边的元素;( 第一轮结束后,序列最后一个元素一定是当前序列的最大值)
- 对序列当中剩下的n-1个元素再次执行步骤1;
- 对于长度为n的序列,一共需要执行n-1轮比较;(利用while循环可以减少执行次数)
#include <stdio.h>
#include <string.h> //能够使用strlen()函数
int main()
{
char a[10] = { 12 ,43,9,13,67,98,101,89,3,35 };//十个数的无序数列
int temp,len;
len = sizeof(a);
//冒泡排序
for (int i = 0; i < len-1 ; i++)//n个数的数列总共扫描n-1次,每次把最大的数排到后面,然后再拍前面没排过的
{
for (int j = 0; j < len - i -1; j++)//每一趟扫描到a[n-i-2]与a[n-i-1]比较为止结束
{
if (a[j] > a[j + 1])//后一位数比前一位数小的话,就交换两个数的位置(升序)
{
temp = a[j + 1];
a[j + 1] = a[j];
a[j] = temp;
}
}
}
for (int i = 0; i < 10; i++)
{
printf("%d ", a[i]);
}
getchar(); //执行执行完毕后不立即返回代码窗口,等待回车后再返回,便于查看运行结果。
return 0;
}
1.3.1 快速排序
快速排序的基本思想:挖坑填数+分治法
- 先从数列中取出一个数作为基准数;(在这里选择序列当中第一个数最为基准数)
- 分区过程,将比这个数大的数全放到它的右边,小于或等于它的数全放到它的左边;
- 再对左右区间重复第第一步、二步,直到各区间只有一个数;
用伪代码描述如下:
- i =L; j = R; 将基准数挖出形成第一个坑a[i]。
- j–由后向前找比它小的数,找到后挖出此数填前一个坑a[i]中。
- i++由前向后找比它大的数,找到后也挖出此数填到前一个坑a[j]中
- 再重复执行2,3二步,直到i==j,将基准数填入a[i]中
- 然后再用以上同样的方法排序K左边的数,和K右边的数
#include <stdio.h>
#include <string.h> //能够使用strlen()函数
void QuickSort(char* arr, int low, int high)
{
if (low < high)
{
int i = low;
int j = high;
int k = arr[low];
while (i < j)
{
while (i < j && arr[j] >= k) // 从右向左找第一个小于k的数
{
j--; //如果找不到,就往前一格继续找
}
if (i < j) //把找出来的小于K的数,放在前面,第一个小于K的数放在第一个,第二个小于K的数放在第二个,以此类推
{
arr[i] = arr[j]; //把找出来的最小的数放到a[i]的位置
i++; //i往后移一格
}
while (i < j && arr[i] < k) // 从左向右找第一个大于等于k的数
{
i++; //如果找不到,就往后一格继续找
}
if (i < j) //把找出来的大于K的数,放在后面,第一个大于K的数放在最后一个,第二个大于K的数放在倒数第二个,以此类推
{
arr[j] = arr[i]; //把找出来的最大的数放到a[j]的位置
j--; //j往后移一格
}
}
//到此为止,小于K的值都在K左边;大于K的值都在K右边
arr[i] = k; //将基准值放入剩下的那个坑里面
// 递归调用
QuickSort(arr, low, i - 1); // 排序k左边的数
QuickSort(arr, i + 1, high); // 排序k右边的数
}
}
// 主函数
int main()
{
int len;
char array[10] = { 12,85,25,16,34,23,49,95,17,61 };
len = sizeof(array);
QuickSort(array, 0, len - 1); // 快速排序
for (int i = 0; i < len; i++) //打印出来
{
printf("%d ", array[i]);
}
getchar(); //执行执行完毕后不立即返回代码窗口,等待回车后再返回,便于查看运行结果。
return 0;
}
1.4 归并排序
- 归并排序是建立在归并操作上的一种有效的排序算法,该算法是采用分治法的一个典型的应用。它的基本操作是:将已有的子序列合并,达到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并, 使用中牺牲空间换取时间的算法
- 归并排序其实要做两件事:
- 拆分----将序列每次折半拆分;
- 合并----将划分后的序列段两两排序合并;
因此,归并排序实际上就是两个操作,拆分+合并
拆分:
合并:
- 如何分解?
在这里,我采用递归的方法,首先将待排序列分成A,B两组;然后重复对A、B序列分组;直到分组后组内只有一个元素,此时我们认为组内所有元素有序,则分组结束; - 如何合并?
arr[first,…,mid]为第一段,arr[mid+1,…,last]为第二段,并且两端已经有序,现在我们要将两端合成达到temp[first…last]并且也有序:
- 首先依次从第一段与第二段中取出元素比较,将较小的元素赋值给temp[];
- 重复执行上一步,当某一段赋值结束,则将另一段剩下的元素赋值给temp[];
- 此时将temp[]中的元素复制给arr[],则得到的arr[first…last]有序;
- 代码解析
#include<stdio.h>
#include<string.h>
#define ArrLen 20
void merge(char arr[], int start, int mid, int end) //对区间进行排序
{
int result[ArrLen]; //用来进行两个区间的合并
int temp = 0; //用来存储合并以后有序的序列
int i = start; //第一个区间的开始
int j = mid + 1; //第二个区间的开始
while (i <= mid && j <= end) //把区间分成[start,mid]和[mid+1,end]进行排序
{
if (arr[i] < arr[j]) //比较两个区间的第一个元素谁小,如果区间一的第一个元素较小,则放进result的第一位,然后区间一的第二个元素和区间二的第一个元素比较,谁小就放入result的下一位,以此类推,直到其中一个区间遍历完
{
result[temp++] = arr[i++];
}
else //如果区间二的元素较小,则放进result,以此类推
{
result[temp++] = arr[j++];
}
}
if (i == mid + 1) //如果第一个区已经遍历完了
{
while (j <= end) //并且第二个区还没有遍历完
result[temp++] = arr[j++]; //将第二个区间的值按顺序排在result后面
}
if (j == end + 1) //如果第二个区已经遍历完了
{
while (i <= mid) //并且第一个区还没有遍历完
result[temp++] = arr[i++]; //将第一个区间的值按顺序排在result后面
}
for (j = 0, i = start; j < temp; i++, j++) //把排序好的数组result[]放入arr[]数组中
{
arr[i] = result[j];
}
}
void mergeSort(char arr[], int start, int end)
{
if (start >= end) //如果区间只剩下一个值,则停止递归,返回0
{
return;
}
int mid = (start + end) / 2; //取中间值
mergeSort(arr, start, mid); //用中间值把去区间分成[start,min]和[min+1,end]
mergeSort(arr, mid + 1, end); //用中间值把去区间分成[start,min]和[min+1,end]
merge(arr, start, mid, end); //每次把分出来区间进行排序
}
int main()
{
int len;
char arr[] = { 4, 7, 6, 5, 2, 1, 8, 2, 9, 1 };
len = sizeof(arr);
mergeSort(arr, 0, len-1);
for (int i = 0; i < 10; i++) //打印出来
{
printf("%d ", arr[i]);
}
getchar(); //执行执行完毕后不立即返回代码窗口,等待回车后再返回,便于查看运行结果
// system("pause");
return 0;
}
1.5 基数排序
基数排序(以整形为例),
- 将整形10进制按每位拆分,然后从低位到高位依次比较各个位。每次比较完进行排序,直到整个数组有序
主要分为两个过程:
- 分配: 先从个位开始,根据位值(0-9)分别放到0~9号桶中(比如:53,个位为3,则放入3号桶中;187,个位为7,则放入7号桶中)
- 收集: 再将放置在0~9号桶中的数据按顺序放到数组中
- 重复(1)(2)过程,从个位到最高位,直到排好序为止(比如32位无符号整形最大数4294967296,最高位为10位)
打个比方:
-
假设有欲排数据序列:73 22 93 43 55 14 28 65 39 81
-
首先根据个位数的数值,在遍历数据时将它们各自分配到编号0至9的桶(个位数值与桶号一一对应)中。
分配结果(逻辑想象)如下图所示:
-
分配结束后,接下来将所有桶中所盛数据按照桶号由小到大(桶中由顶至底)依次重新收集串起来
-
得到如下仍然无序的数据序列:81 22 73 93 43 14 55 65 28 39
-
接着,再进行一次分配,这次根据十位数值来分配(原理同上),分配结果(逻辑想象)如下图所示:
-
分配结束后,接下来再将所有桶中所盛的数据(原理同上)依次重新收集串接起来
-
得到如下的数据序列:14 22 28 39 43 55 65 73 81 93
-
观察可以看到,此时原无序数据序列已经排序完毕
-
如果排序的数据序列有三位数以上的数据,则重复进行以上的动作直至最高位数为止
#include<stdio.h>
#include<stdlib.h>
#define Len 10 //数组个数
#define RADIX_10 10 //整形排序,共有有10个桶分别对应着0-9
#define KeyNum 10 //关键字个数,这里为整形位数,最高可以筛选到10个位(个位、十位、百位、千位......)
// 找到num的从低到高的第pos位的数据
int GetNumInPos(int num, int pos)
{
int temp = 1;
for (int i = 0; i < pos - 1; i++)//筛选出num数组中的个位或十位或百位......
{
temp *= 10;
}
return (num / temp) % 10; //返回找出来的那个位
}
void RadixSort(int* arr, int arr_Len)
{
int* temp[RADIX_10]; //分别为0~9的序列空间,通俗来说就是有10个桶
int dat; //用来存放temp[num]中存了多少个数
int num; //用来存放通过函数GetNumInPos()返回来的那个值
for (int i = 0; i < 10; i++) //为temp分配空间
{
temp[i] = (int*)malloc(sizeof(int) * (arr_Len + 1)); //为数组temp分配空间
temp[i][0] = 0; //每个temp[i][0]都是用来记录该temp[i]中放入了多少个数据
}
for (int pos = 1; pos <= KeyNum; pos++) //从个位开始,总共循环10个位
{
for (int i = 0; i < arr_Len; i++) //分配过程,把arr[]中的每个数都分配一次
{
num = GetNumInPos(arr[i], pos); //得到arr[i]中的pos位
temp[num][0]++; //temp[num]中加了一个数,所以temp[num]的第0位加1
dat = temp[num][0]; //把temp[num]中存了多少个数告诉dat
temp[num][dat] = arr[i]; //将数据放入temp[num]的第dat位
}
for (int i = 0, j = 0; i < 10; i++) //收集
{
for (int k = 1; k <= temp[i][0]; k++) //把分配好的按顺序返回arr[]数组中
{
arr[j] = temp[i][k];
j++;
}
temp[i][0] = 0; //并将用来记录个数的位给复位
}
}
}
int main()
{
int arr_test[Len] = { 854, 424, 227, 38, 524, 124, 635, 922, 140, 75 };
RadixSort(arr_test, Len); //基数排序
for (int i = 0; i < Len; i++) //打印出来
{
printf("%d ", arr_test[i]);
}
getchar(); //执行执行完毕后不立即返回代码窗口,等待回车后再返回,便于查看运行结果。
return 0;
}
动态分配玩内存要用释放~ 不然可能会因此丢了工作