本文主要介绍冒泡排序、选择排序、插入排序、希尔排序、归并排序、计数排序。
冒泡排序
算法介绍
- 比较相邻的元素。如果第一个比第二个大或者小,就交换他们两个。
- 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。在这一点,最后的元素应该会是最大或最小的数。
- 针对所有的元素重复以上的步骤,除了最后一个。
- 持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。
基本思想
设数组长度为N:
- 比较相邻的前后两个数据,如果前面数据大于或小于后面的数据,就将二个数据交换。
- 这样对数组的第0个数据到N-1个数据进行一次遍历后,最大的一个数据就“升”到数组第N-1个位置。
- N=N-1,如果N不为0就重复前面二步,直至排序完成。
//为了方便数组元素交换,定义一个交换函数 void myswap(int *a, int *b) { int temp = *a; *a = *b; *b = temp; } //if (arr[j] < arr[j + 1])从大到小 //if (arr[j] > arr[j + 1])从小到大 for (int i = 0; i < len - 1; i++) for (int j = 0; j < len - 1 - i; j++) if (arr[j] < arr[j + 1]) myswap(&arr[j], &arr[j + 1]);
如上是冒泡排序最简洁的程序实现,虽然可以实现排序,但是依然存在可以优化的点。
缺点1:当序列已经排序好,但外层循环未结束时,仍然会继续进行比较,无法及时退出循环,浪费了一定的时间。
缺点2:在进行比较时,若一部分序列已经有序,已经有序的那一部分每次仍然会重复进行比较,也浪费了一定的时间
对代码进行优化后:
int x;
int lastpos=len-1;//记录每一轮循环最后一次交换的位置的下标,即为有序序列的边界,首次无序的边界为最后一个元素的下标
for (int i = 0; i < len-1 ; i++)
{
int flag = 1;//作为判断当前序列是否有序,
for (int j = 0; j < lastpos; j++)
if (arr[j] < arr[j + 1])//if (arr[j] > arr[j + 1])从小到大
{
myswap(&arr[j], &arr[j + 1]);
flag = 0;//发生了交换,flag置为0,说明序列任然无序
x= j;//把无序数列的边界更新为最后一次交换的下标
}
lastpos = x;
if (flag == 1)//没有发生交换,说明已经有序,直接退出
{
break;
}
}
优化后的冒泡排序可以大幅度降低排序所花的时间,因为避免了很多不必要的比较。另外值得注意的是,冒泡排序是通过交换元素来实现排序的,并且只要两个数不相等,就需要定义一个中间变量来实现交换,交换是一种特别浪费时间的方式,故冒泡排序的使用场景一般为数据量不大的序列,对于较大的序列使用冒泡排序时间复杂度将会比较高。
选择排序
算法介绍:
每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到全部待排序的数据元素排完。
基本思想
设数组为a[0…n-1]。
- 初始时,数组全为无序区为a[0..n-1],令i=0
- 在无序区a[i…n-1]中选取一个最小的元素,将其与a[i]交换。交换之后a[0…i]就形成了一个有序区。
- i++并重复第二步直到i==n-1。排序完成。
算法实现:
//值交换函数
void myswap1(int* a, int* b)
{
int temp = *a;
*a = *b;
*b = temp;
}
//从大到小
for (int i = 0; i < len-1; i++)
{
int max = i;//记录最大或最小值下标,第一次默认第一个为最大值或最小值
for (int j = i+1; j < len; j++)
if (arr[max] < arr[j])//if (arr[min] > arr[j ])从小到大
max = j;//改变最大值下标
if (max != i)//下标发生了改变
myswap1(&arr[max], &arr[i]);
}
选择排序是不稳定的排序方法,选择排序效率:O(n²)
插入排序
算法介绍
每次将一个待排序的元素,按其值大小插入到前面已经排好序的子序列中的适当位置,直到全部记录插入完成为止。
基本思想
设数组为a[0…n-1]。
- 初始时,a[0]自成1个有序区,无序区为a[1..n-1]。令i=1
- 将a[i]插入当前的有序区a[0…i-1]中形成a[0…i]的有序区间。
- i++并重复第二步直到i==n-1。排序完成。
算法实现
//从大到小排列
for (int i = 1; i < len; i++)
{
if (arr[i - 1] < arr[i])//加了判断条件后可以减少内层循环不必要的执行
{
int j, temp = arr[i];//j需要定义在for循环外面,不然出了循环体就无法使用j了
for (j = i - 1; j >= 0; j--)
if (temp > arr[j])
arr[j + 1] = arr[j];
else
break;
arr[j + 1] = temp;
}
}
希尔排序
算法介绍
希尔排序的实质就是分组插入排序,该方法又称缩小增量排序,因DL.Shell于1959年提出而得名。
基本思想
先将整个待排元素序列分割成若干个子序列(由相隔某个“增量”的元素组成的)分别进行直接插入排序,然后依次缩减增量再进行排序,待整个序列中的元素基本有序(增量足够小)时,再对全体元素进行一次直接插入排序。因为直接插入排序在元素基本有序的情况下(接近最好情况),效率是很高的,因此希尔排序在时间效率上比前三种方法有较大提高。
算法实现
int w = len / 4;//步长
while (w)
{
for (int k = 0; k < w; k++)//再嵌套一个循环,可以保证即使步长较大也能访问到每一个元素
{
for (int i =k+w; i < len; i += w)
{
int j, temp = arr[i];//j需要定义在for循环外面,不然出了循环体就无法使用j了
for (j = i - w; j >= 0; j -= w)
if (temp < arr[j])
arr[j + w] = arr[j];
else
break;
arr[j + w] = temp;
}
}
w /= 2;
}
希尔排序适用于数据量较大且数据相对有序的序列,可以节省时间,若数据量较小,直接简单插入排序就可以了。
关于希尔排序步长的说明
步长的计算公式可以自行制定,最后步长 == 1即可。
通过大量测试得出的结论:步长 = 步长 / 3 + 1
快速排序
算法介绍
快速排序是C.R.A.Hoare于1962年提出的一种划分交换排序。它采用了一种分治的策略,通常称其为分治法(Divide-and-ConquerMethod)。
分治法基本思想
- 先从数列中取出一个数作为基准数。
- 分区过程将比这个数大的数全放到它的右边,小于或等于它的数全放到它的左边。
- 再对左右区间重复第二步,直到各区间只有一个数。
算法实现
简而言之,在待排序的数列中,我们首先要找一个数字作为基准数。为了方便,我们一般选择第 1 个数字作为基准数。接下来我们需要把这个待排序的数列中小于基准数的元素移动到待排序的数列的左边,把大于基准数的元素移动到待排序的数列的右边。这时,左右两个分区的元素就相对有序了;使用递归的方式,接着把两个分区的元素分别按照上面两种方法继续对每个分区找出基准数,然后移动,直到各个分区只有一个数时为止。并且每一次的分区就能确定一个元素在排序后序列的具体位置。
void swap(int* a, int* b)
{
int temp = *a;
*a = *b;
*b = temp;
}
void quickly_sort(int arr[], int l, int r)
{
if (l >= r)
return;
int m = l;
int x = r;
while (l < r)
{
while ((arr[r] >= arr[m]) && (r > l))
r--;
while ((arr[l] <= arr[m]) && (r > l))
l++;
if (l < r)
swap(&arr[l], &arr[r]);
else
swap(&arr[m], &arr[r]);
}
quickly_sort(arr, m, l - 1);
quickly_sort(arr, l + 1,x);
}
快速排序是在冒泡排序的基础上改进而来的,冒泡排序每次只能交换相邻的两个元素,而快速排序是跳跃式的交换,交换的距离很大,因此总的比较和交换次数少了很多,速度也快了不少。
快速排序是一个不稳定的算法,在经过排序之后,可能会对相同值的元素的相对位置造成改变。快速排序基本上被认为是相同数量级的所有排序算法中,平均性能最好的。
归并排序
算法介绍
归并排序是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。
基本思想
基本思路就是将数组分成二组A,B,如果这二组组内的数据都是有序的,那么就可以很方便的将这二组数据进行排序。那么如何让这二组组内数据有序,就是归并排序的主要问题所在。
可以将A,B组各自再分成二组。依次类推,当分出来的小组只有一个数据时,可以认为这个小组组内已经达到了有序,然后再合并相邻的二个小组就可以了。这样通过先递归的分解数列,再合并数列就完成了归并排序。
如何合并连个有序序列
只要从比较二个数列的第一个数,谁小就先取谁,取了后就在对应数列中删除这个数。然后再进行比较,如果有数列为空,那直接将另一个数列的数据依次取出即可。
算法实现
void merge_array(int arr[], int f, int mid, int l, int temp[])
{
int leftstart = f;//左有序序列起始位置
int leftend = mid;//左有序序列结束位置
int rightstart = mid + 1;//右有序序列起始位置
int rightend = l;/右有序序列结束位置
int len = 0;//序列长度
int i = leftstart, j = rightstart;
while ((i <= leftend) &&( j <= rightend))
{
if (arr[i] <= arr[j])
{
temp[len++] = arr[i++];
}
else
{
temp[len++] = arr[j++];
}
}
while(i<=leftend)//左序列元素没有取完
temp[len++] = arr[i++];
while(j<=rightend)//右序列元素没有取完
temp[len++] = arr[j++];
for (i = 0; i < len; ++i)
arr[leftstart+ i] = temp[i];//元素复制到arr序列中
}
void mergesort(int arr[], int f, int l, int temp[])
{
if (f < l)
{
int mid = (f + l) / 2;//将序列进行二分
mergesort(arr, f, mid, temp);//二分左序列
mergesort(arr, mid + 1, l, temp);//二分右序列
merge_array(arr, f, mid, l, temp);//归并两个有序序列
}
}
计数排序
算法介绍
计数排序(counting sort)就是一种牺牲内存空间来换取低时间复杂度的排序算法,同时它也是一种不基于比较的算法。这里的不基于比较指的是数组元素之间不存在比较大小的排序算法。
算法实现
所谓计数排序,就是统计待排序序列每一个元素的出现次数,创建一个大小为待排序序列中最大值大小的序列count,以待排序序列的元素值作为count的下标,记录待排序序列的各个元素出现的次数,例如,若arr数组中元素99一共出现了3次,则count[99]=3;最后遍历count即可输出arr排序后的结果。
int arr[10] = { 33,44,88,88,99,99,44,33,5,9 };
int max = arr[0];
int len = sizeof(arr) / sizeof(int);
for (int i = 1; i < len; i++)
if (max < arr[i])
max = arr[i];//找到最大值
//定义计数数组,并且全部置空
int* count=malloc(sizeof(int)*(max+1));
for (int i = 0; i <=max; i++)
count[i] = 0;
//统计待排序系列中各元素的数量
for (int i = 0; i < 10; i++)
++count[arr[i]];
//按序输出序列
for (int i = max; i >= 0;i--)
while (count[i]>0)
{
printf("%d ", i);
count[i]--;
}
虽然计数排序看上去很强大,但是它存在两大局限性:
1.当数列最大最小值差距过大时,并不适用于计数排序
比如给定 20 个随机整数,范围在 0 到 1 亿之间,此时如果使用计数排序的话,就需要创建长度为 1 亿的数组,不但严重浪费了空间,而且时间复杂度也随之升高。
2.当数列元素不是整数时,并不适用于计数排序
如果数列中的元素都是小数,比如 3.1415,或是 0.00000001 这样子,则无法创建对应的统计数组,这样显然无法进行计数排序。正是由于这两大局限性,才使得计数排序不像快速排序、归并排序那样被人们广泛适用。