排序算法
一.交换排序
交换排序的基本思想都为通过比较两个数的大小,当满足某些条件时对它进行交换从而达到排序的目的。
1.1 冒泡排序
基本思想:比较相邻的两个数,如果前者比后者大,则进行交换。每一轮排序结束,选出一个未排序中最大的数放到数组后面。
最差时间复杂度为O( n^2 ),平均时间复杂度为O(n^2)。稳定性:稳定。辅助空间O(1)。
注:稳定性指:如果在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,r[i]=r[j],且r[i]在r[j]之前,而在排序后的序列中,r[i]仍在r[j]之前,则称这种排序算法是稳定的;否则称为不稳定的。
/**
* 冒泡排序,从小到大
* 时间复杂度:O(n*n)
* 思想:比较相邻的两个数,如果前者比后者大,则进行交换。每一轮排序结束,选出一个未排序中最大的数放到数组后面
*/
void bubble_sort(DataType a[], int n)
{
int i,j;
DataType t;
if (NULL == a)
return;
for (i = 0; i < n; i++)
{
for (j = 0; j < n-i-1; j++)
{
//如果前面的数比后面大,进行交换
if (a[j] > a[j+1])
{
t = a[j];
a[j] = a[j+1];
a[j+1] = t;
}
}
}
}
升级版冒泡排序法:通过从低到高选出最大的数放到后面,再从高到低选出最小的数放到前面,如此反复,直到左边界和右边界重合。当数组中有已排序好的数时,这种排序比传统冒泡排序性能稍好。
/**
* 升级版冒泡排序,从小到大
* 时间复杂度:O(n*n)
* 思想:通过从低到高选出最大的数放到后面,再从高到低选出最小的数放到前面,如此反复,
直到左边界和右边界重合。当数组中有已排序好的数时,这种排序比传统冒泡排序性能稍好。
*/
void bubble_plus_sort(DataType a[], int n)
{
int left,right;
int j;
DataType t;
if (NULL == a)
return;
left = 0;
right = n-1;
while (left < right)
{
//从左到右遍历选出最大的数放到数组右边
for (j = left; j < right; j++)
{
if (a[j] > a[j+1])
{
t = a[j];
a[j] = a[j+1];
a[j+1] = t;
}
}
right--;
//从右到左遍历选出最小的数放到数组左边
for (j = right; j > left; j--)
{
if (a[j] < a[j-1])
{
t = a[j];
a[j] = a[j-1];
a[j-1] = t;
}
}
left++;
}
}
1.2.快速排序
基本思想:
- 选取一个基准元素,通常为数组最后一个元素(或者第一个元素)
- 分区过程,将比这个数大的数全放到它的右边,小于或等于它的数全放到它的左边
- 在利用分治策略从已经分好的两组中分别进行以上步骤,直到排序完成
复杂度:
最差时间复杂度:每次选取的基准元素都为最大(或最小元素)导致每次只划分了一个分区,需要进行n-1次划分才能结束递归,故复杂度为O(n^2);最优时间复杂度:每次选取的基准元素都是中位数,每次都划分出两个分区,需要进行logn次递归,故时间复杂度为O(nlogn);平均时间复杂度:O(nlogn)。稳定性:不稳定的。辅助空间:O(nlogn)。
当数组元素是有序时,快速排序将没有任何优势,基本退化为冒泡排序,可以把数组打乱
/**
* [min,max]之间的随机数
*/
unsigned int randint(unsigned int min, unsigned int max)
{
if (min > max)
{
unsigned int t = min;
min = max;
max = min;
}
return RAND(max-min+1)+min;
}
/**
* 快速排序,从小到大
* 时间复杂度:O(n*logn)
* @algorithm:分治法
* @param a: 数组指针
l: 数组的左边开始下标
r: 数组的右边结束下标
*/
void quick_sort(DataType a[], int l, int r)
{
int i, j;
DataType t;
if(l < r)
{
//如果数组已经是有序的,快速排序的效率很低,所以,先把数组打乱
swap(a[l],a[randint(l,r)]);
i = l;
j = r;
t = a[i];
while(i < j)
{
while ((i < j) && (a[j] > t))//从右往左找到小于x的数
{
j--;
}
if (i < j)
{
a[i++] = a[j];
}
while((i < j) && (a[i] < t)) //从左往右找到大于x的数
{
i++;
}
if (i < j)
{
a[j--] = a[i];
}
}
a[i] = t; //i = j的时候,将x填入中间位置
quick_sort(a,l,i-1); //对左边排序
quick_sort(a,i+1,r);
}
}
二.插入排序
2.1.直接插入排序
基本思想:和交换排序不同的是它不用进行交换操作,而是用一个临时变量存储当前值。当前面的元素比后面大时,先把后面的元素存入临时变量,前面元素的值放到后面元素位置,再到最后把其值插入到合适的数组位置。
复杂度:最坏时间复杂度为数组为逆序时,为O(n *n)。最优时间复杂度为数组正序时,为O(n)。平均时间复杂度为O(n^2)。辅助空间O(1)。稳定性:稳定。
/**
* 插入排序,从小到大
* 时间复杂度:O(n*n)
* @param a: 数组指针
n: 数组元素个数
*/
void insert_sort(DataType a[], int n)
{
int i,j;
DataType t;
if (NULL == a)
return;
for (i = 1; i < n; i++)
{
t = a[i];
//一次比较t左面的数,如果大于t,右移
for (j = i; ((j > 0) && (t < a[j-1])); j--)
{
a[j] = a[j-1];
}
a[j] = t;
}
}
2.2 希尔(shell)排序
基本思想:
希尔排序是把记录按下标的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至1时,整个文件恰被分成一组,算法便终止。
简单插入排序很循规蹈矩,不管数组分布是怎么样的,依然一步一步的对元素进行比较,移动,插入,比如[5,4,3,2,1,0]这种倒序序列,数组末端的0要回到首位置很是费劲,比较和移动元素均需n-1次。而希尔排序在数组中采用跳跃式分组的策略,通过某个增量将数组元素划分为若干组,然后分组进行插入排序,随后逐步缩小增量,继续按组进行插入排序操作,直至增量为1。希尔排序通过这种策略使得整个数组在初始阶段达到从宏观上看基本有序,小的基本在前,大的基本在后。然后缩小增量,到增量为1时,其实多数情况下只需微调即可,不会涉及过多的数据移动。
我们来看下希尔排序的基本步骤,在此我们选择增量gap=length/2,缩小增量继续以gap = gap/2的方式,这种增量选择我们可以用一个序列来表示,{n/2,(n/2)/2…1},称为增量序列。希尔排序的增量序列的选择与证明是个数学难题,我们选择的这个增量序列是比较常用的,也是希尔建议的增量,称为希尔增量,但其实这个增量序列不是最优的。此处我们做示例使用希尔增量。
参考:希尔排序算法详解
复杂度:
最坏时间复杂度为O(n2);最优时间复杂度为O(n);平均时间复杂度为O(n1.3)。辅助空间O(1)。稳定性:不稳定。希尔排序的时间复杂度与选取的增量有关,选取合适的增量可减少时间复杂度。
/**
* 希尔排序,从小到大
* 时间复杂度:O(n*n)
* 思想:希尔排序是把数组按下标的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,
每组包含的关键词越来越多,当增量减至1时,整个数组恰被分成一组,算法便终止。
*/
void shell_sort(DataType a[], int n)
{
int i,j;
DataType t;
int increment;
while(increment > 1)
{
increment /= 2; //增量公式,我还看到过用 increment=increment/3+1作为公式
// 插入排序
for(i = increment; i < n; i++)
{
t = a[i];
for(j = i; ((j >= increment) && (t < a[j-increment])); j=j-increment)
{
a[j] = a[j-increment];
}
a[j] = t;
}
}
}
三.选择排序
3.1.直接选择排序
基本思想:依次选出数组最小的数放到数组的前面。首先从数组的第二个元素开始往后遍历,找出最小的数放到第一个位置。再从剩下数组中找出最小的数放到第二个位置。以此类推,直到数组有序。
复杂度:最差、最优、平均时间复杂度都为O(n^2)。辅助空间为O(1)。稳定性:不稳定。
/**
*选择排序,从小到大
*时间复杂度:O(n*n)
*/
void select_sort(DataType a[], int n)
{
int key;
int i,j;
DataType t;
for (i = 0; i < n -1; i++)
{
key = i;
for (j = i+1; j < n; j++)
{
if (a[j] < a[key])
{
key = j; // 记录数组最小值位置
}
}
if (key != i)
{
//交换i和key的位置
t = a[i];
a[i] = a[key];
a[key] = t;
}
}
}
3.2.堆(Heap)排序
/**
* 当a[0,...,i]是堆,改变a[i](最后一个节点),使重新成为一个堆
* @param i: 堆的根节点
*/
void sift_up(DataType a[], int i)
{
int child,father;
DataType t;
//数组是从0开始计数,所以左子节点为father=(child+1)/2-1
// 创建大顶堆(父节点大于子节点)
child = i;
father = (child+1)/2 - 1;
while(father >= 0)
{
//如果子节点大于父节点,交换
if (a[child] > a[father])
{
t = a[child];
a[child] = a[father];
a[father] = t;
}
else
{
break;
}
child = father;
father = (child+1)/2 - 1;
}
}
/**
* 当a[i,...,n-1]是堆,改变a[i](把a[i]看成根节点),使重新成为一个堆
* @param i: 堆的根节点
n: 堆的右边界
*/
void sift_down(DataType a[], int i, int n)
{
int child,father;
DataType t;
//数组是从0开始计数,所以左子节点为2*i+1,右子节点为2*i+2
// 创建大顶堆(父节点大于子节点)
father = i;
child = 2*i+1; //左子节点
while(child < n)
{
if ((child + 1) < n) //右子节点存在
{
//找出左右节点中的最大值
if (a[child+1] > a[child])
{
child++;
}
}
//如果子节点大于父节点,交换
if (a[child] > a[father])
{
t = a[child];
a[child] = a[father];
a[father] = t;
}
else
{
break;
}
//子节点已经改变,把当前的子节点作为父节点,继续上面的过程
father = child;
child = 2*father+1;
}
}
/**
* 堆排序,从小到大
* 时间复杂度:O(n*logn)
* 思想:
*/
void heap_sort(DataType a[], int n)
{
int i;
DataType t;
if (NULL == a && n < 1)
return;
//数组是从0开始计数,所以左子节点为2*i+1,右子节点为2*i+2
// 1. 创建大顶堆(父节点大于子节点)
for (i = n/2-1; i >= 0; i--)
{
sift_down(a,i,n);
}
//2.排序
//a[0]是堆的最大值,把a[0]和a[i]交换,那么a[i]-a[n-1]是有序的
for(i = n-1; i > 0; i--)
{
//交换a[0]和a[i]
t = a[i];
a[i] = a[0];
a[0] = t;
//堆的大小减一,重建堆
sift_down(a,0,i);
}
}
四.归并排序
五.总结
其中排序算法总结如下:
参考资料:七大经典排序算法总结(C语言描述)