排序算法总结 | 数组

下面对常见的排序算法,包括四种简单排序算法:冒泡排序、选择排序、插入排序和希尔排序;三种平均时间复杂度都是
nlogn的高级排序算法:快速排序、归并排序和堆排序,进行全方面的总结,其中包括代码实现、时间复杂度及空间复杂度分
析和稳定性分析,最后对以上算法进行较大数据量下的排序测试,验证其时间性能。

1. 简单排序算法

1.1 冒泡排序

思想:从后往前,两两比较,将较小的元素交换至前方,一直重复下去,第一遍排序将数组中最小的元素放到了数组的最前
端;同理,第二遍则将数组中第一个元素之后的最小元素交换到第二的位置,以此类推…整个过程可以形象地看作是较小元素
如同“泡泡”一样往上浮,故名冒泡排序( Bubble Sort) .

 

程序实现

public class BubbleSort {
	public void bubbleSort(int[] nums) {
		if (nums == null || nums.length == 0) {
			return;
		}

		for (int i = nums.length - 1; i > 0; i--) {
			boolean flag = true; // optimize
			for (int j = 0; j < i; j++) {
				if (nums[j] > nums[j + 1]) {
					swap(nums, j, j + 1);
					flag = false;
				}
			}
			if (flag) {
				break; // if there is no exchange, the array is sorted
			}
		}
	}

	private void swap(int[] nums, int j, int i) {
		int tmp = nums[j];
		nums[j] = nums[i];
		nums[i] = tmp;
	}
}

 

时间/空间复杂度分析

最好情况下,数组中的元素为正序,比较次数为n-1 次,交换次数为0次,时间复杂度为O(n);最坏情况下,数组中的元素为逆
序,需要n-1+n-2+…+2+1 = n(n – 1)/2次比较和同样多次数的交换,时间复杂度为O(n^2);平均时间复杂度为O(n^2).
空间复杂度为O(1).


稳定性

基于相邻元素间交换的算法是稳定的。

 

适用条件

编程实现最为简单,但效率很低,只限于小规模数据。

 

1.2 选择排序

思想: 每次扫描数组,记录最小元素的下标,扫描完成后将最小元素与第一个元素进行交换,即第一个元素为最小元素,然后
以此类推,直到找完所有剩余元素中的最小元素,交换完成为止。

 

程序实现

public class SelectSort {
	public void selectSort(int[] nums) {
		if (nums == null || nums.length == 0) {
			return;
		}

		for (int i = 0; i < nums.length; i++) {
			int min = nums[i];
			int minIdx = i;
			for (int j = i + 1; j < nums.length; j++) {
				if (nums[j] < min) {
					min = nums[j];
					minIdx = j;
				}
			}
			if (minIdx != i) {
				swap(nums, i, minIdx);
			}
		}
	}
	
	private void swap(int[] nums, int j, int i) {
		int tmp = nums[j];
		nums[j] = nums[i];
		nums[i] = tmp;
	}
}

 

时间/空间复杂度分析

最好情况需要n*(n – 1)/2次比较, 0次交换,时间复杂度为O(n^2);最坏情况下为n*(n – 1)/2次比较, n次交换,交换次数比冒
泡排序更少(通常交换操作比比较操作更消耗CPU的运行时间),时间复杂度为O(n^2);平均时间复杂度为O(n^2).
空间复杂度为O(1).

 

稳定性

不稳定,例如对于序列5 8 5 2 9,第一次5和2进行交换,此时5的位置在第二个5的后面,之前的顺序遭到破坏,因而不稳定。

 

适用条件

小规模数据的排序。

1.3 插入排序

思想: 通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。插入排序在实现上,通常
采用in-place排序(即只需用到O(1)的额外空间的排序),因而在从后向前扫描过程中,需要反复把已排序元素逐步向后挪
位,为最新元素提供插入空间。

 

程序实现

public class InsertSort {
	public void insertSort(int[] nums) {
		if (nums == null || nums.length == 0) {
			return;
		}

		for (int i = 1; i < nums.length; i++) {
			if (nums[i] < nums[i - 1]) {
				int tmp = nums[i];
				int j = i;
				while (j > 0 && nums[j - 1] > nums[j]) {
					nums[j] = nums[j - 1];
					--j;
				}
				nums[j] = tmp;
			}
		}
	}
}

 

时间/空间复杂度分析

最好情况是元素构成正序,只需要n-1 次比较,不需要挪动元素的位置,时间复杂度为O(n);最坏情况下是元素构成逆序,需
要n-1 次比较和n*(n-1)/2次元素的挪动,时间复杂度为O(n^2);平均时间复杂度为O(n^2).
空间复杂度为O(1).

 

稳定性
稳定

 

适用条件

插入排序非常适合小数据量的排序工作,在STL的sort算法和stdlib的qsort算法中,都将插入排序作为快速排序的补充,用于少
量元素的排序(通常为8个或以下)。

1.4 希尔排序

思想: 希尔排序是插入排序的一种高速的改进版本,基本思想是先取一个小于n的整数d1 作为第一个增量,把文件的全部记录分成d1 个组。所有距离为dl的倍数的记录放在同一个组中。先在各组内进行直接插人排序;然后,取第二个增量d2<d1重复上述的分组和排序,直至所取的增量dt=1(dt< dt-l<…<d2<d1),即所有记录放在同一组中进行直接插入排序为止。

 

程序实现

public class ShellSort {
	public void shellSort(int[] nums) {
		if (nums == null || nums.length == 0) {
			return;
		}

		for (int gap = nums.length / 2; gap > 0; gap /= 2) {
			for (int i = gap; i < nums.length; i += gap) {
				int tmp = nums[i];
				if (nums[i] < nums[i - gap]) {
					int j = i;
					while (j - gap >= 0 && nums[j - gap] > nums[j]) {
						nums[j] = nums[j - gap];
						j -= gap;
					}
					nums[j] = tmp;
				}
			}
		}
	}
}

 

时间/空间复杂度分析
时间复杂度为O(nlogn^2),大约为O(n^1.3),比起前三种简单排序算法快得多。
空间复杂度为O(1).

 

稳定性
不稳定,单趟的插入排序是稳定的,但是不同组的插入排序有可能打乱原有的元素顺序。

 

适用条件

虽然比不上时间复杂度为O(n*logn)的高级排序算法快,但在中等规模的数据集上仍然表现不错,并且编程较简单。甚至有些专家们提倡,几乎任何排序工作在开始时都可以用希尔排序,若在实际使用中证明它不够快, 再改成快速排序这样更高级的排序算法.

 

2  高级排序算法

2.1 快速排序算法

思想: 快速排序是冒泡排序的一种改进,交换顺序不再限于相邻元素间。基本思想为通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。

 

实现
有递归和非递归两种实现方式,其中partition函数是共用的。值得说明的是,后续的测试表明, 递归版本的快排在运行时间上要优于非递归版本。

public class QuickSort {
	// recursion
	public void quickSortRec(int[] nums) {
		if (nums == null || nums.length == 0) {
			return;
		}

		recHelper(nums, 0, nums.length - 1);
	}

	private void recHelper(int[] nums, int begin, int end) {
		if (begin >= end) {
			return;
		}
		int mid = partition(nums, begin, end);
		recHelper(nums, begin, mid - 1);
		recHelper(nums, mid + 1, end);
	}

	private int partition(int[] nums, int low, int high) {
		int begin = low - 1, end = high;
		int pivot = nums[end];

		while (true) {
			while (begin < end && nums[++begin] <= pivot) {
				;
			}
			while (begin < end && nums[--end] >= pivot) {
				;
			}
			if (begin >= end) {
				break;
			}
			swap(nums, begin, end);
		}
		swap(nums, begin, high);
		return begin;
	}

	private void swap(int[] nums, int begin, int end) {
		int tmp = nums[begin];
		nums[begin] = nums[end];
		nums[end] = tmp;
	}

	// no recursion
	public void quickSortNoRec(int[] nums) {
		if (nums == null || nums.length == 0) {
			return;
		}

		Stack<Integer> s = new Stack<Integer>();
		s.push(0);
		s.push(nums.length - 1);

		while (!s.isEmpty()) {
			int high = s.pop();
			int low = s.pop();
			int mid = partition(nums, low, high);

			if (mid > low) {
				s.push(low);
				s.push(mid - 1);
			}
			if (mid < high) {
				s.push(mid + 1);
				s.push(high);
			}
		}
	}
}

 

时间/空间复杂度分析

最好情况是,每执行一次分割,都能将数组分为两个长度近乎相等的片段,然后这样递归下去,递推式为T(n) = 2*T(n/2) + O(n),其中O(n)为一次partition的时间消耗,因此最好和平均时间复杂度均为O(nlogn);最坏情况下,数组元素为逆序,此时的递推式退化为T(n) = T(n – 1) + O(n),时间复杂度为O(n^2).
空间复杂度上,尽管快排是in-place的,但递归需要一定的空间消耗,最好情况下, logn级别次数的递归调用,将消耗O(logn)的空间;最坏情况下,则是n级别次数的递归调用,此时的空间复杂度为O(n).


稳定性
不稳定,中枢元素与对应元素交换时将可能打乱数组的原本顺序。


适用条件
平均上看,快排的时间性能最好,适用于中大规模的数据排序。有许多种方法可以尽量避免快排的最坏情况,如每次随机选择枢纽元素,或者一开始选择首中尾中的中值元素作为枢纽元素等。

2.2 归并排序算法


思想: 是建立在归并操作上的一种有效的排序算法,是采用分治法( Divide and Conquer)的一个非常典型的应用。每个递归过程涉及三个步骤 :
第一, 分解: 把待排序的 n 个元素的序列分解成两个子序列, 每个子序列包括 n/2 个元素.
第二, 治理: 对每个子序列分别调用归并排序MergeSort, 进行递归操作
第三, 合并: 合并两个排好序的子序列,生成排序结果.


实现

public class MergeSort {
	private int[] copy;

	// recursion
	public void mergeSortRec(int[] nums) {
		if (nums == null || nums.length == 0) {
			return;
		}
		copy = new int[nums.length];
		mergeSortRecHelper(nums, 0, nums.length - 1);
	}

	private void mergeSortRecHelper(int[] nums, int begin, int end) {
		if (begin >= end) {
			return;
		}
		int mid = begin + (end - begin) / 2;
		mergeSortRecHelper(nums, begin, mid);
		mergeSortRecHelper(nums, mid + 1, end);
		mergeArrays(nums, begin, mid, end);
	}

	private void mergeArrays(int[] nums, int begin, int mid, int end) {
		int low = begin, high = mid + 1;
		int k = begin;

		while (low <= mid && high <= end) {
			if (nums[low] < nums[high]) {
				copy[k++] = nums[low++];
			} else {
				copy[k++] = nums[high++];
			}
		}

		while (low <= mid) {
			copy[k++] = nums[low++];
		}
		while (high <= end) {
			copy[k++] = nums[high++];
		}

		// copy to origin array
		for (int i = begin; i <= end; i++) {
			nums[i] = copy[i];
		}
	}

	// no recursion
	public void mergeSortNoRec(int[] nums) {
		if (nums == null || nums.length == 0) {
			return;
		}
		
		copy = new int[nums.length];
		
		int step = 2;
		while (true) {
			int start = 0;
			while (start < nums.length) {
				int end = start + step - 1;
				if (end > nums.length - 1) {
					end = nums.length - 1;
				}
				int mid = start + (end - start) / 2;
				mergeArrays(nums, start, mid, end);
				start = end + 1;
			}
			// important statement
			if (step > nums.length) {
				break;
			}
			step *= 2;
		}
	}
}

 

时间/空间复杂度分析
各种情况下的时间复杂度均为O(nlogn)
空间复杂度为O(n)

稳定性
稳定


适用条件
中等规模的数据量,大规模的数据将受到内存限制(空间复杂度)。

2.3 堆排序算法


思想:首先需要清楚二叉堆的定义,二叉堆是完全二叉树或者是近似完全二叉树,堆的存储一般都用数组实现。
二叉堆满足以下2个特性:
1 .父结点的键值总是大于或等于(小于或等于)任何一个子节点的键值。
2.每个结点的左子树和右子树都是一个二叉堆(都是最大堆或最小堆)。
当父结点的键值总是大于或等于任何一个子节点的键值时为最大堆。当父结点的键值总是小于或等于任何一个子节点的键值时为最小堆。


堆排序的思想是,先对整个数组堆化处理,形成最大堆(最终形成升序的序列),此时位于数组首位的元素为最大,将其换至末尾,此时调整整个堆(即所有除了末尾以外的元素),调整完后首尾元素又是当前的最大元素,将其换至倒数第二个位置,以此类推,直到整个序列有序(升序)为止。


实现

public class HeapSort {
	public void heapSort(int[] nums) {
		if (nums == null || nums.length == 0) {
			return;
		}

		// build the max-heap of array
		buildMaxHeap(nums, nums.length);
		heapSortHelper(nums);
	}

	private void heapSortHelper(int[] nums) {
		for (int i = nums.length - 1; i > 0; i--) {
			swap(nums, 0, i);
			fixMaxHeap(nums, 0, i);
		}
	}

	private void swap(int[] nums, int i, int j) {
		int tmp = nums[i];
		nums[i] = nums[j];
		nums[j] = tmp;
	}

	private void buildMaxHeap(int[] nums, int n) {
		for (int i = n / 2 - 1; i >= 0; i--) {
			fixMaxHeap(nums, i, n);
		}
	}

	private void fixMaxHeap(int[] nums, int i, int n) {
		int tmp = nums[i];
		int j = 2 * i + 1;

		while (j < n) {
			if (j + 1 < n && nums[j + 1] > nums[j]) {
				// choose the max between left and right
				j++;
			}
			if (nums[j] <= tmp) {
				break;
			}
			nums[i] = nums[j];
			i = j;
			j = 2 * j + 1;
		}
		nums[j] = tmp;
	}
}

 

时间/空间复杂度分析


建堆的时间复杂度为O(n),调整一次堆的时间为O(logn),排序过程中对n-1 个元素进行了调整操作,最终的时间复杂度依然为O(nlogn).
空间复杂度为O(1).

[建堆时间复杂度O(n): http://blog.sina.com.cn/s/blog_691a84f301014aze.html]


稳定性
堆排序是不稳定的:
比如: 3 27 36 27,
如果堆顶3先输出,则,第三层的27(最后一个27)跑到堆顶,然后堆稳定,继续输出堆顶,是刚才那个27,这样说明后面的
27先于第二个位置的27输出,不稳定。


适用条件
大规模数据量

3. 各个排序算法的比较测试


结论:
(1)  简单排序中,希尔排序的时间性能最好,插入排序次之,冒泡排序性能最差;
(2)  三种高级排序的时间性能:快速排序 > 归并排序 > 堆排序,递归版本的快排性能较非递归要好,而对于归并排序而言,非递归版本性能较好。

4. 排序算法总结图

(图片来源: http://www.cnblogs.com/biyeymyhjob/archive/2012/07/17/2591457.html)


参考资料
1. 排序算法汇总总结: http://www.cnblogs.com/biyeymyhjob/archive/2012/07/17/2591457.html
2. 白话经典排序算法系列: http://blog.csdn.net/morewindows/article/details/6709644/

posted @ 2015-06-25 09:16  Chapter  阅读(245)  评论(0编辑  收藏  举报