常见排序算法总结
一、冒泡排序
1、定义
冒泡排序是一种比较简单的排序算法,它会遍历若干次要排序的数列,每次便利时,它都会从前往后依次的比较两个相邻的数的大小;如果前者比后者大,则交换它们的位置。
这样一次遍历之后,最大的元素就在数列的末尾了。采用相同的方法在此遍历时,第二大的元素就被排列在最大元素之前,重复此操作,直到整个数列都有序为止。
冒泡排序的时间复杂度为 :O(N2)-稳定
2、实现(C++)
void bubbleSort(int *arr, int len)
{
int temp;
int flag; //标志位
//注意i和j的取值范围是重点
for(int i = len-1;i > 0; i++)
{
flag = 0;//初始化标志为为0
//将arr[0……i]中最大的数据放在末尾
for(int j = 0; j < i; j++)
{
if(arr[j] > arr[j+1])
{
//交换两个数
temp = arr[j];
arr[j] = arr[j+1];
arr[j+1] = temp;
//若发生交换,则将标志为置为1
flag = 1;
}
}
//若没有发生交换,则说明数列已有序
if(flag == 0)
break;
}
}
二、快速排序
1、定义
快速排序(quick sort)使用分治法策略。
它的基本思想是:选择一个基准数,通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都小,然后,再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数列变成有序数列。
快速排序的流程:
(1)从数列中选择一个基准值
(2)将所有比基准值小的数据放在基准值前面,所有比基准值大的数据放在基准值后面(相同的数据可以放在一边),在这个分区结束后,该基准值就处于数列的中间位置了。
(3)递归的把基准值前面的子数列和基准值后面的子数列进行快速排序。
快速排序的时间复杂度:最坏的情况下是O(n2),平均情况是O(nlog2n)-不稳定
2、实现(C++)
//2、快速排序
void quickSort(int *arr, int left, int right)
{
if(left < right)
{
int midTemp;//基准值
int i = left;
int j = right;
midTemp = arr[0]; //将arr[0]作为基准值
while(i < j)
{
while(i < j && arr[j] > midTemp)
j--;//从后往前找第一个小于midTemp的值
if(i < j)
a[i++] = a[j];
while(i < j && arr[i] < midTemp)
i++;//从前往后找第一个大于midTemp的值
if(i < j)
arr[j--] = arr[i];
}
arr[i] = midTemp;
//递归调用
quickSort(arr, left, i-1);
quickSort(arr, i+1, right);
}
}
三、直接插入排序
1、定义
直接插入排序(Straight insertion Sort)的基本思想是:把n个待排序的元素看成一个有序表和一个无序表,开始时有序表中只包含一个元素,无序表中包含n-1个元素,排序过程中,每次从无序表中取出第一个元素,将它插入到有序表中的适当位置,使之成为一个新的有序表,重复n-1次可完成整个排序过程。
直接插入排序的时间复杂度:O(n2)-稳定
2、实现(C++)
//3、直接插入排序
void insertSort(int *arr, int len)
{
for(int i = 1; i < len; i++)
{
//为arr[i]在前面arr[0 ... i-1]有序区间中找到一个合适的位置
for(int j = i - 1; j >= 0; j--)
{
if(arr[j] < arr[i])
break;
}
//若找到一个合适的位置
if(j != i -1)
{
//将比arr[i]大的数据向后移
int temp = arr[i];
for(int k = i - 1; k > j; k--)
arr[k+1] = arr[k];
//将arr[i]放到正确的位置上
arr[k+1] = temp;
}
}
}
四、希尔排序
1、定义
希尔排序(Shell Sort)是插入排序的一种,它是针对直接插入排序算法的改进。该方法又称缩小增量排序。
希尔排序实质上是一种分组插入方法。它的基本思想是:对于n个待排序的数列,取一个小于n的整数gap(gap被称为步长)将待排序元素分成若干个组子序列,所有距离为gap的倍数的记录放在同一个组中;然后,对各组内的元素进行直接插入排序。 这一趟排序完成之后,每一个组的元素都是有序的。然后减小gap的值,并重复执行上述的分组和排序。重复这样的操作,当gap=1时,整个数列就是有序的。
希尔排序时间复杂度:希尔排序的时间复杂度与增量(即,步长gap)的选取有关。例如,当增量为1时,希尔排序退化成了直接插入排序,此时的时间复杂度为O(N2),而Hibbard增量的希尔排序的时间复杂度为O(N3/2)。-不稳定
Hbbard增量序列: h = 1,3,7,,2^k-1^
hbbard增量序列的特点是:相邻增量没有公因子,最坏的运行时间为O(N^3/2^)
2、实现(C++)
/*
* 希尔排序
* 参数说明:
* arr -- 待排序的数组
* n -- 数组的长度
*/
void shellSort(int* arr, int n)
{
// gap为步长,每次减为原来的一半。
for (int gap = n / 2; gap > 0; gap /= 2)
{
// 共gap个组,对每一组都执行直接插入排序
for (int i = 0 ;i < gap; i++)
{
for (int j = i + gap; j < n; j += gap)
{
// 如果a[j] < a[j-gap],则寻找a[j]位置,并将后面数据的位置都后移。
if (arr[j] < arr[j - gap])
{
int tmp = arr[j];
int k = j - gap;
while (k >= 0 && a[k] > tmp)
{
arr[k + gap] = arr[k];
k -= gap;
}
arr[k + gap] = tmp;
}
}
}
}
}
五、选择排序
1、定义
选择排序(Selection sort)是一种简单直观的排序算法。它的基本思想是:首先在未排序的数列中找到最小(or最大)元素,然后将其存放到数列的起始位置;接着,再从剩余未排序的元素中继续寻找最小(or最大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。
选择排序时间复杂度:选择排序的时间复杂度是O(N2)。
假设被排序的数列中有N个数。遍历一趟的时间复杂度是O(N),需要遍历多少次呢?N-1!因此,选择排序的时间复杂度是O(N2)。-不稳定
2、实现(C++)
/*
* 选择排序
* 参数说明:
* a -- 待排序的数组
* n -- 数组的长度
*/
void selectSort(int* a, int n)
{
int min; // 无序区中最小元素位置
for(int i=0; i<n; i++)//有序区的末尾位置
{
min=i;
// 找出"a[i+1] ... a[n]"之间的最小元素,并赋值给min。
for(j=i+1; j<n; j++)// 无序区的起始位置
{
if(a[j] < a[min])
min=j;
}
// 若min!=i,则交换 a[i] 和 a[min]。
// 交换之后,保证了a[0] ... a[i] 之间的元素是有序的。
if(min != i)
{
int tmp = a[i];
a[i] = a[min];
a[min] = tmp;
}
}
}
六、堆排序
1、定义
堆排序(Heap Sort)是指利用堆这种数据结构所设计的一种排序算法。我们知道,堆分为"最大堆"和"最小堆"。最大堆通常被用来进行"升序"排序,而最小堆通常被用来进行"降序"排序。
鉴于最大堆和最小堆是对称关系,理解其中一种即可。本文将对最大堆实现的升序排序进行详细说明。
最大堆进行升序排序的基本思想:
① 初始化堆:将数列a[1…n]构造成最大堆。
② 交换数据:将a[1]和a[n]交换,使a[n]是a[1…n]中的最大值;然后将a[1…n-1]重新调整为最大堆。 接着,将a[1]和a[n-1]交换,使a[n-1]是a[1…n-1]中的最大值;然后将a[1…n-2]重新调整为最大值。 依次类推,直到整个数列都是有序的。
数组实现的二叉堆的性质:
在第一个元素的索引为 0 的情形中:
性质一:索引为i的左孩子的索引是 (2*i+1);
性质二:索引为i的左孩子的索引是 (2*i+2);
性质三:索引为i的父结点的索引是 floor((i-1)/2);
堆排序的时间复杂度:O(n*log2n) - 不稳定
2、实现(C++)
/*
* 最大堆实现升序排序
* 参数说明:
* a -- 待排序的数组
* start -- 被下调节点的起始位置(一般为0,表示从第1个开始)
* end -- 截至范围(一般为数组中最后一个元素的索引)
*/
void maxheap_down(int a[], int start, int end)
{
int c = start; // 当前(current)节点的位置
int l = 2*c + 1; // 左(left)孩子的位置
int tmp = a[c]; // 当前(current)节点的大小
for (; l <= end; c=l,l=2*l+1)
{
// "l"是左孩子,"l+1"是右孩子
if ( l < end && a[l] < a[l+1])
l++; // 左右两孩子中选择较大者,即m_heap[l+1]
if (tmp >= a[l])
break; // 调整结束
else // 交换值
{
a[c] = a[l];
a[l]= tmp;
}
}
}
七、归并排序
1、定义
将两个的有序数列合并成一个有序数列,我们称之为"归并"。归并排序(Merge Sort)就是利用归并思想对数列进行排序。根据具体的实现,归并排序包括"从上往下"和"从下往上"2种方式。
1、从下往上的归并排序:将待排序的数列分成若干个长度为1的子数列,然后将这些数列两两合并;得到若干个长度为2的有序数列,再将这些数列两两合并;得到若干个长度为4的有序数列,再将它们两两合并;直接合并成一个数列为止。这样就得到了我们想要的排序结果。
2、从上往下的归并排序:它与"从下往上"在排序上是反方向的。它基本包括3步:
① 分解 – 将当前区间一分为二,即求分裂点 mid = (low + high)/2;
② 求解 – 递归地对两个子区间a[low…mid] 和 a[mid+1…high]进行归并排序。递归的终结条件是子区间长度为1。
③ 合并 – 将已排序的两个子区间a[low…mid]和 a[mid+1…high]归并为一个有序的区间a[low…high]。
归并排序时间复杂度:归并排序的时间复杂度是O(Nlog2N)。
假设被排序的数列中有N个数。遍历一趟的时间复杂度是O(N),需要遍历多少次呢?
归并排序的形式就是一棵二叉树,它需要遍历的次数就是二叉树的深度,而根据完全二叉树的可以得出它的时间复杂度是O(Nlog2N)。- 稳定
2、实现(C++)
/*
* 将一个数组中的两个相邻有序区间合并成一个
*
* 参数说明:
* a -- 包含两个有序区间的数组
* start -- 第1个有序区间的起始地址。
* mid -- 第1个有序区间的结束地址。也是第2个有序区间的起始地址。
* end -- 第2个有序区间的结束地址。
*/
void merge(int* a, int start, int mid, int end)
{
int *tmp = new int[end-start+1]; // tmp是汇总2个有序区的临时区域
int i = start; // 第1个有序区的索引
int j = mid + 1; // 第2个有序区的索引
int k = 0; // 临时区域的索引
while(i <= mid && j <= end)
{
if (a[i] <= a[j])
tmp[k++] = a[i++];
else
tmp[k++] = a[j++];
}
while(i <= mid)
tmp[k++] = a[i++];
while(j <= end)
tmp[k++] = a[j++];
// 将排序后的元素,全部都整合到数组a中。
for (i = 0; i < k; i++)
a[start + i] = tmp[i];
delete[] tmp;
}
/*
* 归并排序(从上往下)
*
* 参数说明:
* a -- 待排序的数组
* start -- 数组的起始地址
* endi -- 数组的结束地址
*/
void mergeSortUp2Down(int* a, int start, int end)
{
if(a==NULL || start >= end)
return ;
int mid = (end + start)/2;
mergeSortUp2Down(a, start, mid); // 递归排序a[start...mid]
mergeSortUp2Down(a, mid+1, end); // 递归排序a[mid+1...end]
// a[start...mid] 和 a[mid...end]是两个有序空间,
// 将它们排序成一个有序空间a[start...end]
merge(a, start, mid, end);
}
/*
* 对数组a做若干次合并:数组a的总长度为len,将它分为若干个长度为gap的子数组;
* 将"每2个相邻的子数组" 进行合并排序。
*
* 参数说明:
* a -- 待排序的数组
* len -- 数组的长度
* gap -- 子数组的长度
*/
void mergeGroups(int* a, int len, int gap)
{
int i;
int twolen = 2 * gap; // 两个相邻的子数组的长度
// 将"每2个相邻的子数组" 进行合并排序。
for(i = 0; i+2*gap-1 < len; i+=(2*gap))
{
merge(a, i, i+gap-1, i+2*gap-1);
}
// 若 i+gap-1 < len-1,则剩余一个子数组没有配对。
// 将该子数组合并到已排序的数组中。
if ( i+gap-1 < len-1)
{
merge(a, i, i + gap - 1, len - 1);
}
}
/*
* 归并排序(从下往上)
*
* 参数说明:
* a -- 待排序的数组
* len -- 数组的长度
*/
void mergeSortDown2Up(int* a, int len)
{
int n;
if (a==NULL || len<=0)
return ;
for(n = 1; n < len; n*=2)
mergeGroups(a, len, n);
}
八、桶排序
1、定义
桶排序(Bucket Sort)的原理很简单,它是将数组分到有限数量的桶里。
假设待排序的数组a中共有N个整数,并且已知数组a中数据的范围[0, MAX)。在桶排序时,创建容量为MAX的桶数组r,并将桶数组元素都初始化为0;将容量为MAX的桶数组中的每一个单元都看作一个"桶"。
在排序时,逐个遍历数组a,将数组a的值,作为"桶数组r"的下标。当a中数据被读取时,就将桶的值加1。例如,读取到数组a[3]=5,则将r[5]的值+1。
桶排序的时间复杂度:
对于N个待排数据,M个桶,平均每个桶[N/M]个数据的桶排序平均时间复杂度为:
O(N)+O(M*(N/M)log2(N/M))=O(N+N(log2N-log2M))=O(N+Nlog2N-Nlog2M)
当N=M时,即极限情况下每个桶只有一个数据时。桶排序的最好效率能够达到O(N)。- 稳定
2、实现(C++)
/*
* 桶排序
* 参数说明:
* a -- 待排序数组
* n -- 数组a的长度
* max -- 数组a中最大值的范围
*/
void bucketSort(int a[], int n, int max)
{
int i,j;
int buckets[max];
// 将buckets中的所有数据都初始化为0。
memset(buckets, 0, max*sizeof(int));
// 1. 计数
for(i = 0; i < n; i++)
buckets[a[i]]++;
// 2. 排序
for (i = 0, j = 0; i < max; i++)
{
while( (buckets[i]--) >0 )
a[j++] = i;
}
}