排序是数据处理中十分常见且核心的操作,虽说实际项目开发中很小几率会需要我们手动实现,毕竟每种语言的类库中都有n多种关于排序算法的实现。但是了解这些精妙的思想对我们还是大有裨益的。本文简单温习下最基础的三类算法:选择,冒泡,插入。
先定义个交换数组元素的函数,供排序时调用
简单选择排序
简单选择排序是最简单直观的一种算法,基本思想为将序列分为有序序列和待排的无序序列,每一趟从后面n-i个待排数据元素中选择最小的一个元素作为有序序列的第i个元素,直到所有元素排完为止。
在算法实现时,每一趟确定最小元素的时候会通过不断地比较交换来使得首位置为当前最小,交换是个比较耗时的操作。其实我们很容易发现,在还未完全确定当前最小元素之前,这些交换都是无意义的。我们可以通过设置一个变量k,每一次比较仅存储较小元素的数组下标,当轮循环结束之后,那这个变量存储的就是当前最小元素的下标,此时再执行交换操作即可。代码实现很简单,一起来看下。
例如对无序表{56,12,80,91,20}
采用简单选择排序算法进行排序,具体过程为:
- 第一次遍历时,从下标为 1 的位置即 56 开始,找出关键字值最小的记录 12,同下标为 0 的关键字 56 交换位置:
- 第二次遍历时,从下标为 2 的位置即 56 开始,找出最小值 20,同下标为 2 的关键字 56 互换位置:
- 第三次遍历时,从下标为 3 的位置即 80 开始,找出最小值 56,同下标为 3 的关键字 80 互换位置:
- 第四次遍历时,从下标为 4 的位置即 91 开始,找出最小是 80,同下标为 4 的关键字 91 互换位置:
- 到此简单选择排序算法完成,无序表变为有序表。
代码实现
void println(int array[], int len)
{
int i = 0;
for(i=0; i<len; i++)
{
printf("%d ", array[i]);
}
printf("\n");
}void swap(int array[], int i, int j)
{
int temp = array[i];
array[i] = array[j];
array[j] = temp;
}void SelectionSort(int array[], int len) // O(n*n)
{
int i = 0;
int j = 0;
int k = -1;
for(i=0; i<len; i++)
{
k = i;//循环遍历求出最小值,对应下标赋值为k
for(j=i; j<len; j++)
{
if( array[j] < array[k] )
{
k = j;
}
}//交换a[i]与a[k]的值
swap(array, i, k);
}
}int main()
{
int array[] = {56,12,80,91,20
};
int len = sizeof(array) / sizeof(*array);
println(array, len);
SelectionSort(array, len);
println(array, len);
return 0;
}
简单选择排序通过上面优化之后,无论数组原始排列如何,比较次数是不变的;对于交换操作,在最好情况下也就是数组完全有序的时候,无需任何交换移动,在最差情况下,也就是数组倒序的时候,交换次数为n-1次。综合下来,时间复杂度为O(n2)
直接插入排序
直接插入排序基本思想是每一步将一个待排序的记录,插入到前面已经排好序的有序序列中去,直到插完所有元素为止。最开始认为首元素为有序序列,当插入第i个数据元素时候,认为前面的r[0]、r[1],..., r[i-1]已经排好序,用r[i]与r[i-1],r[i-2],...分别比较,
找到插入位置将r[i]插入到指定位置。
例如采用直接插入排序算法将无序表{3,1,7,5,2,4,9,6}
进行升序排序的过程为:
- 首先考虑记录 3 ,由于插入排序刚开始,有序表中没有任何记录,所以 3 可以直接添加到有序表中,则有序表和无序表可以如图 1 所示:
图 1 直接插入排序(1)
- 向有序表中插入记录 1 时,同有序表中记录 3 进行比较,1<3,所以插入到记录 3 的左侧,如图 2 所示:
图 2 直接插入排序(2)
- 向有序表插入记录 7 时,同有序表中记录 3 进行比较,3<7,所以插入到记录 3 的右侧,如图 3 所示:
图 3 直接插入排序(3)
- 向有序表中插入记录 5 时,同有序表中记录 7 进行比较,5<7,同时 5>3,所以插入到 3 和 7 中间,如图 4 所示:
图 4 直接插入排序(4)
- 向有序表插入记录 2 时,同有序表中记录 7进行比较,2<7,再同 5,3,1分别进行比较,最终确定 2 位于 1 和 3 中间,如图 5 所示:
图 5 直接插入排序(5)
- 照此规律,依次将无序表中的记录 4,9 和 6插入到有序表中,如图 6 所示:
图 6 依次插入记录4,9和6
直接插入排序的具体代码实现为:
代码实现
void InertionSort(int array[], int len) // O(n*n)
{
int i = 0;
int j = 0;
int k = -1;
int temp = -1;
for(i=1; i<len; i++)
{
k = i;
temp = array[k];
for(j=i-1; (j>=0) && (array[j]>temp); j--)
{
array[j+1] = array[j]; //后移空出插入位置
k = j;
}
array[k] = temp; //将待插入记录数据元素插入到已排好序的有序序列中
}
}
简单插入排序在最好情况下,需要比较n-1次,无需交换元素,时间复杂度为O(n);在最坏情况下,时间复杂度依然为O(n2)。但是在数组元素随机排列的情况下,插入排序还是要优于上面两种排序的。
冒泡排序
冒泡排序的基本思想是,对相邻的元素进行两两比较,顺序相反则进行交换,这样,每一趟会将最大或者最小的元素“浮”到顶端,最终达到完全有序。下面的例子每次是将最小元素浮到顶端,在第i趟中从后往前,j=len-1, len-2, ..., i+1两两比较array[j] 与array[j-1] ,如果发生逆序,则交换array[j] 与array[j-1]。
代码实现
在冒泡排序的过程中,如果某一趟执行完毕,没有做任何一次交换操作,比如数组[5,4,1,2,3],执行了两次冒泡,也就是两次外循环之后,分别将5和4调整到最终位置[1,2,3,4,5]。此时,再执行第三次循环后,一次交换都没有做,这就说明剩下的序列已经是有序的,排序操作也就可以完成了,来看下代码
void BubbleSort(int array[], int len) // O(n*n)
{
int i = 0;
int j = 0;
int exchange = 1;
for(i=0; (i<len) && exchange; i++)
{
exchange = 0;
for(j=len-1; j>i; j--)
{
if( array[j] < array[j-1] )
{
swap(array, j, j-1);
exchange = 1;
}
}
}
}
根据上面这种冒泡实现,若原数组本身就是有序的(这是最好情况),仅需n-1次比较就可完成;若是倒序,比较次数为 n-1+n-2+...+1=n(n-1)/2,交换次数和比较次数等值。所以,其时间复杂度依然为O(n2)。综合来看,冒泡排序性能还还是稍差于上面那种选择排序的。
总结
本文列举了排序算法中最基本的三种算法(简单选择,冒泡,插入),这三种排序算法的时间复杂度均为O(n2)