这是经典排序算法系列的第三篇,讲的是选择交换算法。交换排序主要包括:冒泡排序算法,快速排序算法。在这篇博文中,我会从简单的冒泡排序算法讲起,然后慢慢过渡到快速排序算法,让你更容易理解快速排序算法中的精髓。冒泡排序算法只是作为简单的过渡,所以我们的重点在快速排序算法上,其中,包括不同版本的快速排序算法,还有对快速排序算法做出的改进,使得快速排序算法效率更高。
从最左边的记录开始,对每两个相邻的关键字进行比较,且使关键字较大的记录换至关键字较小的记录之上,使得经过一趟冒泡排序后,关键字最大的记录到达最右端,接着,再在剩下的记录中找关键字次大的记录,并把它换到右往左的第二个位置上,依次类推,一直到所有记录都有序为止。
在这里,我会循循善诱,从最简单的基本步骤来引导大家写出代码。从冒泡排序算法的基本思想中我们可以看出,其核心内容是相邻的两个元素进行比较大小,然后再交换位置。相信大家在学习C语言不久就会接触到交换元素的函数,现在让我们回忆一下吧!交换元素的代码如下:
//交换元素
void swap(int *a, int *b)
{
int temp;
temp = *a;
*a = *b;
*b = temp;
}
看到上面的代码很熟悉吧!我们只需要在上面的代码稍作改动就可以实现一趟排序。首先理清思路,要进行一趟排序则必须对所有元素进行一次遍历,在遍历的过程中比较大小,然后将大的元素换到后面的位置。例如对数组a={9,7,0,1,5,6,2}进行一趟排序:
第一次比较:a={9,7,0,1,5,6,2},比较a[0]和a[1],9比7大则交换位置,数组变为a={7,9,0,1,5,6,2}
第二次比较:a={7,9,0,1,5,6,2},比较a[1]和a[2],9比0大则交换位置,数组变为a={7,0,9,1,5,6,2}
第二次比较:a={7,0,9,1,5,6,2},比较a[2]和a[3],9比1大则交换位置,数组变为a={7,0,1,9,5,6,2}
第二次比较:a={7,0,9,1,5,6,2},比较a[3]和a[4],9比7大则交换位置,数组变为a={7,0,1,5,9,6,2}
第二次比较:a={7,0,1,5,9,6,2},比较a[4]和a[5],9比6大则交换位置,数组变为a={7,0,1,5,6,9,2}
第二次比较:a={7,0,1,5,6,9,2},比较a[5]和a[6],9比2大则交换位置,数组变为a={7,9,0,1,5,2,9}
经过一趟排序完毕,我们可以看出最大元素移动到最右端了,也就是说每经过一趟排序,最大元素始终会移动到无序区的最后一位,这样经过n-1趟排序,这个数组就能完成排序。由上述实例,我们可以写出冒泡排序算法的核心代码:
//冒泡排序算法
void bubbleSort(int *a, int length)
{
int i, j;
int temp;
//经过length-1排序
for(i = length-1; i > 0; i--)
{
//遍历一次数组
for(j = 0; j < i-1; j++)
{
//如果元素值大的在前面就与后面元素交换位置
if(a[j]>a[j+1])
{
temp = a[j];
a[j] = a[j+1];
a[j+1] = temp;
}
}
}
}
若记录序列的初始状态为"正序",则冒泡排序过程只需进行一趟排序,在排序过程中只需进行n-1次比较,且不移动记录;反之,若记录序列的初始状态为"逆序",则需进行n(n-1)/2次比较和记录移动。因此冒泡排序总的时间复杂度为O(n^2)。并且冒泡排序算法是一种稳定的排序算法。
从待排序列中任取一个元素 (例如取第一个) 作为中心,所有比它小的元素一律前放,所有比它大的元素一律后放,形成左右两个子表,此为一趟快速排序;然后再对各子表重新选择中心元素并依此规则调整,直到每个子表的元素只剩一个。此时便有序了。
从快速排序算法的基本思想中我们可以看出,这个算法是一个递归过程,那么我们首先将这个算法进行一趟的排序过程理清就很容易写出这个算法。根据算法描述,我们可以选择第一个元素或者最后一个元素为中心元素,然后对这个数组进行遍历,将数组分成两个部分,在中心元素之前的数组部分小于中心元素,在中心元素之后的数组部分大于中心元素。在算法导论中是这样实现的,现在对数组a={2,8,7,1,3,5,6,4}进行一趟快速排序,我们选最后一个元素a[7]=4作为中心元素,并且定义小于中心元素的数组部分为a[0…i-1],大于中心元素的数组部分为a[i…n],a[i]是大于中心元素数组部分的第一个元素,是实现过程如下:
第一次比较:a={2,8,7,1,3,5,6,4},i=0,a[0]与中心元素比较,2<4,元素2不动,i++,则i=1,a={2,8,7,1,3,5,6,4}
第二次比较:a={2,8,7,1,3,5,6,4},i=1,a[1]与中心元素比较,8>4,元素8不动,i不变,则i=1,a={2,8,7,1,3,5,6,4}
第三次比较:a={2,8,7,1,3,5,6,4},i=1,a[2]与中心元素比较,7>4,元素7不动,i不变,则i=1,a={2,8,7,1,3,5,6,4}
第四次比较:a={2,8,7,1,3,5,6,4},i=1,a[3]与中心元素比较,1<4,元素1与a[i]交换,i++,则i=2,a={2,1,7,8,3,5,6,4}
第五次比较:a={2,1,7,8,3,5,6,4},i=2,a[4]与中心元素比较,3<4,元素3与a[i]交换,i++,则i=3,a={2,1,3,8,7,5,6,4}
第六次比较:a={2,1,3,8,7,5,6,4},i=3,a[5]与中心元素比较,5>4,元素5不动,i不变,则i=3,a={2,1,3,8,7,5,6,4}
第七次比较:a={2,1,3,8,7,5,6,4},i=3,a[6]与元素中心比较,6>4,元素6不动,i不变,则i=3,a={2,1,3,8,7,5,6,4}
比较完毕,将中心元素与a[i]交换位置,则4与8交换位置,a={2,1,3,4,7,5,6,8},一趟排序彻底完成,我们可以看出在中心元素4之前的数组都小于4,在中心元素4之后的数组部分都大于4,剩下的排序就是对两个子数组做同样的事情,直到子数组中的元素数目为一个时,则整个数组完成排序。实现快速排序的核心代码如下:
//将数组划分成两部分,中心元素前数组小于中心元素,中心元素后数组大于中心元素
int partition(int *a,int low, int high)
{
//设置中心元素下标
int i = low;
//存储中心元素的数值
int middle = a[high];
int j;
int temp;
//遍历数组
for(j = 0; j < high-1; j++)
{
//如果遍历元素小于中心元素则将遍历元素换到中心元素之前
if(a[j] <= middle)
{
temp = a[i];
a[i] = a[j];
a[j] = temp;
i++;
}
}
/*将大于中心元素的第一元素与中心元素交换位置,
则中心元素之后元素大于中心元素,
中心元素之前元素小于中心元素 */
a[high] = a[i];
a[i] = middle;
//返回中心元素下标
return i;
}
当我们写完上面的代码时,内心的一块石头便落下来了,因为剩下的工作对我们来说已经是易如反掌了,我们只需要对这个函数进行递归,便可以对两个子数组进行同样的操作,其代码如下:
//快速排序算法
void quickSort(int *a, int low, int high)
{
int i;
if(low < high)
{
//存储中心元素的下标值
i = partition(a,low,high);
//对中心元素之前的子数组快速排序
quickSort(a,low,i-1);
//对中心元素之后的子数组快速排序
quickSort(a,i+1,high);
}
}
数组通过上述代码的递归调用便可以完成排序,网络上还有一个比较流行的快速排序写法,思路和上面的有些不同,上面的算法是通过对数组从左到右遍历,而网络上那种流行的写法是同时从数组的两边往中间遍历,现在我将其源代码展示出来:
//快速排序算法
void quickSort(int *a, int low, int high)
{
int i;
if(low < high)
{
//存储中心元素的下标值
i = partition(a,low,high);
//对中心元素之前的子数组快速排序
quickSort(a,low,i-1);
//对中心元素之后的子数组快速排序
quickSort(a,i+1,high);
}
}
int partition(int *a, int low, int high)
{
int i = low, j = high;
//存储中心元素
int middle = a[high];
int temp;
while(i < j)
{
//从左边遍历数组直到出现比中心元素大的元素
while(a[i]<=middle)
{
i++
}
//从右边遍历数组直到出现比中心元素小的元素
while(a[j]>=middle)
{
j--;
}
//如果数组遍历没有超界,则将大元素换到后面,小元素换到前面
if(i
快速排序算法的最好时间复杂度为O (nlbn),最坏为O(n*n);因为每趟可以确定不止一个元素的位置,而且呈指数增加,所以特别快,就平均计算时间而言,快速排序是我们所讨论的所有内排序方法中最好的一个。但在原记录已经有序时,快速排序反而逊于冒泡排序;不稳定。