七大排序算法(上)

排序

排序的定义非常简单,就是把乱序状态下的众多元素整理成有序状态。关于排序,有稳定和不稳定之分。关于排序算法,有内排序和外排序之分。本文将对内排序的众多算法进行实现和比较。首先,我们来看一下衡量一个算法性能的因素:

(1)时间性能:在内排序算法中,主要有两种操作:比较和移动。衡量一个算法的优劣时,时间性能是最重要的标志。

(2)辅助空间:评价算法好坏的另一个关键因素就是执行算法时所需要的辅助存储空间。此处的辅助空间不包括待排序数据所占的空间,而是指完成排序过程中额外需要的空间。

(3)算法复杂性:指算法本身的复杂度,而不是指算法的时间复杂度,当然两者之间有着不可分割的关系。

一般我们将内排序分为插入排序、交换排序、选择排序和归并排序。本文将讲解七种排序算法:

(1)冒泡排序

(2)简单选择排序

(3)直接插入排序

(4)希尔排序

(5)堆排序

(6)归并排序

(7)快速排序

其中前三种属于简单排序算法,后四种属于改进算法。下面将分别介绍以上七种排序算法。由于交换操作非常常见,此处先列出此函数,以备后用:

private void swap(int[] array, int i, int j) {
	int temp = array[i];
	array[i] = array[j];
	array[j] = temp;
}

(一)冒泡排序

冒泡排序算法几乎是计算机相关专业中路人皆知的排序算法,因其最大值依次向后冒出,就像沸水中的气泡一样依次冒出而得名。简单粗暴,直接上代码就好。

public void bubbleSort(int[] array) {
	boolean flag = true;
    for (int i = 0; i < array.length && flag; i++) {
		flag = false;
		for (int j = array.length - 1; j > i; j--) {
            if (array[j] < array[j - 1]) {
            	swap(array, j, j - 1);
				flag = true;
            }
        }
   }
}

其中唯一需要注意的是算法中设置了一个flag,标志每一步冒泡后该数组是否还需要排序,避免了不必要的比较。冒泡排序的时间复杂度为0+1+···+n-1=(n-1)n/2,也就是O(n*n)。

(二)简单选择排序

大家观察冒泡算法时得知每次比较之后,都要进行交换。其实这是没必要的,因为我一次遍历比较完成后,确认了最小值的位置之后再与第一个位置进行数据交换也不迟。这样虽然比较次数不能节省,但数据交换次数却可以大大节省。简单选择排序其实非常简单,依然简单粗暴,直接看代码:

public void selectSort(int[] array) {
	for (int i = 0; i < array.length; i++) {
		int min = i;        //假设i位置(剩余元素的第一个位置)的元素就是剩余元素的最小值
		for (int j = i + 1; j < array.length; j++)
			if (array[j] < array[min])
				min = j;   //现在的min位置的元素是真正的最小值了
		if (min != i)
			swap(array, i, min); //没猜对的话,就只好把最小值与剩余元素的第一个值交换了
	}
}

简单选择排序算法虽然较冒泡排序少了很多的数据交换,但是比较操作并没有少,因此算法复杂度仍为0+1+···+n-1=(n-1)n/2,也就是O(n*n)。虽然如此,其性能还是略优于冒泡排序。

(三)直接插入排序

直接插入排序算法与前述两算法稍有不同,生活中与其息息相关的就是摸牌、码牌啦。假如我们先把所有牌摸完,再排序。比如说,第一张牌你不需要排序,排第二张时就要考虑应该放到第一张牌的前面还是后面。。。。这样,等到排第n张牌时,前n-1张牌已经有序,你只需从后向前遍历,只要此处的牌比第n张牌大,就将此处的牌后移一位(当然你应该先把第n张牌暂存起来,以免被覆盖掉)。。。直到某张牌不再比第n张牌大,这张牌也就是第n张牌应该放的位置。这样第n张牌就排好啦。再排下一张,直到排完为止。逻辑应该比较简单,还是直接上代码好啦。

public void InsertSort(int[] array) {
	for (int i = 1; i < array.length; i++) { //除第一张牌外的其他所有牌都要参与循环
		if (array[i - 1] > array[i]) {   //只有前一张牌比这张牌大,才需要排序
			int temp = array[i];       //存起来,一会可能被后移的牌覆盖掉
			int index = i - 1;                
			while (index >= 0 && array[index] > temp)//寻找不再比它大的那个位置
				array[index + 1] = array[index--];
			array[index + 1] = temp;     //其实多递减了一次哎
		}
	}
}

辅助空间只有一个,就是暂存第n张牌用到的那个空间。可以推出,平均比较和移动的操作约为n * n / 4 次,算法复杂度为 O(n*n)。另外需要注意的是,最后的index多递减了1次,记得加上啊。

(四)希尔排序

直接插入排序中每次比较之后进行后移操作,确实通俗易懂,但是仔细想想,步子太小(步子为1),一个居于靠后位置的较小的元素小碎步走了很多步才走到该排的地方,如果步子大一点,一次性跨越几个元素,每次都保证基本有序,最后再对一个基本有序的数组进行步长为1的排序,这样会不会省去很多数据交换的操作呢。希尔做了此番尝试,证明果然是酱紫。先上代码:

public void shellSort(int[] array) {
	int increment = array.length;            //步长初始化
	do {
		increment = increment / 3 + 1;   //调整步长,使其逐渐变小,最后一次运行时步长为1。
		for (int i = increment; i < array.length; i++) {  //从步长位置开始遍历,每次不再与前一个比较,而是与前第increment位置的比较,步子是不是变大啦
			if (array[i] < array[i - increment]) {    //以下与直接插入排序相同,只是步长大
				int temp = array[i];
				int index = i - increment;
				while (index >= 0 && array[index] > temp) {
					array[index + increment] = array[index];
					index -= increment;
				}
				array[index + increment] = temp;
			}
		}
	} while (increment > 1);
}

关于步长的计算公式为啥时最好,至今没有定论。此处increment = increment / 3 - 1 的步长减小公式应该是一个性价比较高的公式。此算法时间的复杂度为O(n的3/2次方),好于直接排序的O(n*n)。注意,希尔排序并不是稳定排序,因为它的比较是跳动的。

posted @ 2015-09-23 21:15  torresliang  阅读(181)  评论(0编辑  收藏  举报