Algorithm —— 归并排序(四)

Algorithm —— 归并排序


在计算机科学中,分治法是建基于多项分支递归的一种很重要的算法范式。字面上的解释是“分而治之”,就是把一个复杂的问题分成两个或更多的相同或相似的子问题,直到最后子问题可以简单的直接求解,原问题的解即子问题的解的合并。

这个技巧是很多高效算法的基础,如排序算法(快速排序、归并排序)、傅立叶变换(快速傅立叶变换)。

另一方面,理解及设计分治法算法的能力需要一定时间去掌握。正如以归纳法去证明一个理论,为了使递归能够推行,很多时候需要用一个较为概括或复杂的问题去取代原有问题。而且并没有一个系统性的方法去适当地概括问题。

分治法这个名称有时亦会用于将问题简化为只有一个细问题的算法,例如用于在已排序的列中寻找其中一项的折半搜索算法(或是在数值分析中类似的勘根算法)。这些算法比一般的分治算法更能有效地执行。其中,假如算法使用尾部递归的话,便能转换成简单的循环。但在这广义之下,所有使用递归或循环的算法均被视作“分治算法”。因此,有些作者考虑“分治法”这个名称应只用于每个有最少两个子问题的算法。而只有一个子问题的曾被建议使用减治法这个名称。

分治算法通常以数学归纳法来验证。而它的计算成本则多数以解递回关系式来判定。

归并排序(MERGE-SORT)是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。
归并过程为:比较a[i]和b[j]的大小,若a[i]≤b[j],则将第一个有序表中的元素a[i]复制到r[k]中,并令i和k分别加上1;否则将第二个有序表中的元素b[j]复制到r[k]中,并令j和k分别加上1,如此循环下去,直到其中一个有序表取完,然后再将另一个有序表中剩余的元素复制到r中从下标k到下标t的单元。归并排序的算法我们通常用递归实现,先把待排序区间[s,t]以中点二分,接着把左边子区间排序,再把右边子区间排序,最后把左区间和右区间用一次归并操作合并成有序的区间[s,t]。

归并操作的工作原理如下:
第一步:申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列
第二步:设定两个指针,最初位置分别为两个已经排序序列的起始位置
第三步:比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置
重复步骤3直到某一指针超出序列尾
将另一序列剩下的所有元素直接复制到合并序列尾

《算法导论》中提到,分治法的思想是:将原问题分解为几个规模较小但类似于原问题的子问题,递归地求解这些子问题,然后再合并这些子问题的解来建立原问题的解。

分治模式在每层递归时都有三个步骤:

  • 分解原问题为若干子问题,这些子问题是原问题的规模较小的实例。
  • 解决这些子问题,递归地求解各个子问题。然而,若子问题的规模足够小,则直接求解。
  • 合并这些子问题的解成原问题的解。

归并排序算法完全遵循分治模式。直观上操作如下:

  • 分解:分解待排序的n个元素的序列成个具n/2个元素的两个序列。
  • 解决:使用归并排序递归地排序两个子序列。
  • 合并:合并两个已排序的子序列以产生已排序的答案。

当待排序的序列长度为1时,递归“开始回升”,在这种情况下不要做任何工作,因为长度为1的每个序列都已排好序。

归并排序算法主要的Java代码实现如下:

	/**
	 * 归并排序
	 * 
	 * @param array
	 *            需要排序的数组
	 * @param p
	 *            需要排序的子数组的下标下边界
	 * @param r
	 *            需要排序的子数组的下标上边界
	 * 
	 *            如果p >= r;根据数组的合法性,任务此时需要被排列的数组只有一个元素;而一个元素本身就是有序的,无需排序; 
	 *            如果 p < r;此时需要排序的子数组至少有两个元素,可以进行排序处理
	 */
	public void mergeSort(int[] array, int p, int r) {
		
		if (p > (array.length - 1) || r > (array.length - 1))
			throw new IllegalArgumentException("argument is illegal!");
		
		System.out.println("MergeSort:" + " p: " + p + " r: " + r);
		arrayPrint(array);

		if (p < r) {
			int q = (p + r)/2;//子数组分组的依据
			mergeSort(array, p, q);
			mergeSort(array, q+1, r);
			merge(array, p, q, r);
		}else {
			System.out.println(" --- only one number is treated as sorted...");
		}
		arrayPrint(array);
	}

	/**
	 * 合并两个已经排过序的小子数组,并得到一个新的有序大子数组
	 * 
	 * @param array
	 *            一个数组
	 * @param p
	 *            数组下标边界
	 * @param q
	 *            数组下标边界
	 * @param r
	 *            数组下标边界
	 * 
	 *            p/q/r满足p =< q < r;
	 *            假设子数组array[p...q]和array[q+1...r]都已经排好序,merge(int[], int,int,int)函数的作用就是
	 *            合并这两个已排好序的子数组并得到新的有序子数组array'[p...r],代替当前的子数组array[p...r]
	 *
	 */
	private void merge(int[] array, int p, int q, int r) {

		// 如array[1,6],分成两个子数组a[1,3]和a'[4,6];即此时相当于: p = 1,q = 3,r = 6;
		int num1 = q - p + 1;// array[p...q]子数组的元素个数
		int num2 = r - q;// array[q+1...r]子数组的元素个数

		int[] L = new int[num1 + 1];
		int[] R = new int[num2 + 1];

		for (int i = 0; i < num1; i++) {
			L[i] = array[p + i];// a[1...3]的值存入到L中
		}

		for (int j = 0; j < num2; j++) {
			R[j] = array[q + j + 1];// a[4...6]的值存入R中
		}

		// 哨兵值,可以使算法免于检测数组L/R的访问是否已到下标末尾了;
		// 假设数组L[0...(num1-1)]都已经访问过了,此时L[num1]的值是Integer.MAX_VALUE,它是我们添加的哨兵值;
		// 由于我们数组中的有效值不存在Integer.MAX_VALUE,所以此时L数组的下标值就不会在变了;当数组R[0...(num2-1)]也都已经访问过后,R[num1]也是Integer.MAX_VALUE
		// 但L[num1]/R[num2]不会进行比较,因为我们已经限定了所需要比较的子数组元素的个数是(r - p + 1),此时for循环已经退出;
		L[num1] = Integer.MAX_VALUE;
		R[num2] = Integer.MAX_VALUE;

		int i = 0, j = 0;
		for (int k = p; k <= r; k++) {
			if (L[i] <= R[j]) {// 升序排列
				array[k] = L[i];
				i++;
			} else {
				array[k] = R[j];
				j++;
			}
		}
	}

	public void arrayPrint(int[] array) {
		System.out.print("[");
		for (int i = 0; i < array.length; i++) {
			System.out.print(array[i] + ", ");
		}
		System.out.println("]");
	}
主要测试代码如下:
public class AlgorithmTest {

	public static void main(String[] args) {
		int[] array = {5, 2, 4, 7, 1, 3, 2, 6};
		AlgorithmTest algorithm = new AlgorithmTest();
		algorithm.mergeSort(array, 0, array.length-1);
		System.out.println("final resut:");
		algorithm.arrayPrint(array);
	}

	/**
	 * 归并排序
	 * 
	 * @param array
	 *            需要排序的数组
	 * @param p
	 *            需要排序的子数组的下标下边界
	 * @param r
	 *            需要排序的子数组的下标上边界
	 * 
	 *            如果p >= r;根据数组的合法性,任务此时需要被排列的数组只有一个元素;而一个元素本身就是有序的,无需排序; 
	 *            如果 p < r;此时需要排序的子数组至少有两个元素,可以进行排序处理
	 */
	public void mergeSort(int[] array, int p, int r) {
		
		if (p > (array.length - 1) || r > (array.length - 1))
			throw new IllegalArgumentException("argument is illegal!");
		
		System.out.println("MergeSort:" + " p: " + p + " r: " + r);
		arrayPrint(array);

		if (p < r) {
			int q = (p + r)/2;//子数组分组的依据
			mergeSort(array, p, q);
			mergeSort(array, q+1, r);
			merge(array, p, q, r);
		}else {
			System.out.println(" --- only one number is treated as sorted...");
		}
		arrayPrint(array);
	}

	/**
	 * 合并两个已经排过序的小子数组,并得到一个新的有序大子数组
	 * 
	 * @param array
	 *            一个数组
	 * @param p
	 *            数组下标边界
	 * @param q
	 *            数组下标边界
	 * @param r
	 *            数组下标边界
	 * 
	 *            p/q/r满足p =< q < r;
	 *            假设子数组array[p...q]和array[q+1...r]都已经排好序,merge(int[], int,int,int)函数的作用就是
	 *            合并这两个已排好序的子数组并得到新的有序子数组array'[p...r],代替当前的子数组array[p...r]
	 *
	 */
	private void merge(int[] array, int p, int q, int r) {

		// 如array[1,6],分成两个子数组a[1,3]和a'[4,6];即此时相当于: p = 1,q = 3,r = 6;
		int num1 = q - p + 1;// array[p...q]子数组的元素个数
		int num2 = r - q;// array[q+1...r]子数组的元素个数

		int[] L = new int[num1 + 1];
		int[] R = new int[num2 + 1];

		for (int i = 0; i < num1; i++) {
			L[i] = array[p + i];// a[1...3]的值存入到L中
		}

		for (int j = 0; j < num2; j++) {
			R[j] = array[q + j + 1];// a[4...6]的值存入R中
		}

		// 哨兵值,可以使算法免于检测数组L/R的访问是否已到下标末尾了;
		// 假设数组L[0...(num1-1)]都已经访问过了,此时L[num1]的值是Integer.MAX_VALUE,它是我们添加的哨兵值;
		// 由于我们数组中的有效值不存在Integer.MAX_VALUE,所以此时L数组的下标值就不会在变了;当数组R[0...(num2-1)]也都已经访问过后,R[num1]也是Integer.MAX_VALUE
		// 但L[num1]/R[num2]不会进行比较,因为我们已经限定了所需要比较的子数组元素的个数是(r - p + 1),此时for循环已经退出;
		L[num1] = Integer.MAX_VALUE;
		R[num2] = Integer.MAX_VALUE;

		int i = 0, j = 0;
		for (int k = p; k <= r; k++) {
			if (L[i] <= R[j]) {// 升序排列
				array[k] = L[i];
				i++;
			} else {
				array[k] = R[j];
				j++;
			}
		}
	}

	public void arrayPrint(int[] array) {
		System.out.print("[");
		for (int i = 0; i < array.length; i++) {
			System.out.print(array[i] + ", ");
		}
		System.out.println("]");
	}
}
如果我们测试的数组是这种数组长度n为2的幂时,此次归并排序的过程如下图所示:


合并只有1项的序列对形成长度为2的排序号的序列,合并长度为2的序列对形成长度为4的排序好的序列,依此下去,直到长度为n/2的两个序列被合并最终形成长度为n的排序号的序列。










猜你喜欢

转载自blog.csdn.net/csdn_of_coder/article/details/79950273
今日推荐