演算法與資料結構高階排序演算法之歸併排序

吳軍旗發表於2019-03-03

原文請訪問我的技術部落格番茄技術小站 主要是關於歸併排序演算法,以及一些優化

定義(wiki)

歸併排序(英語:Merge sort,或mergesort),是建立在歸併操作上的一種有效的排序演算法,效率為 {\displaystyle O(n\log n)} {\displaystyle O(n\log n)}(大O符號)。1945年由約翰·馮·諾伊曼首次提出。該演算法是採用分治法(Divide and Conquer)的一個非常典型的應用,且各層分治遞迴可以同時進行。

演示

首先將資料分為兩部分,對這兩部分的資料進行行排序,然後將兩部分排序好的陣列進行歸併(有一種O(N)時間複雜度的方法可以實現)

具體步驟

  • 首先將資料2分為兩步分,直到不可劃分為止

paste image

  • 然後從下往上依次將已經排好序的左右兩邊陣列進行歸併

    paste image

  • 步驟3:

    paste image

  • 步驟4:

    paste image

  • 步驟5:

    paste image

程式碼實現

<?php
// require('../Library/SortTestHelper.php');
require('../SortingBasic/InsertionSort.php');
/**
 * [merge 合併兩個有序的陣列:arr[$l...$mid]和arr[$mid+1, $r]]
 * @param  [type] $arr [description]
 * @param  [type] $l   [description]
 * @param  [type] $mid [description]
 * @param  [type] $r   [description]
 * @return [type]      [description]
 */
function merge(&$arr, $l, $mid, $r){
	$tmp = array();
	$tmp = array_slice($arr, $l, $r-$l+1, true);

	//tmp現在為$arr的副本,以tmp為軸,重新賦值$arr
	$i = $l;
	$j = $mid+1;
	for ($k=$l; $k <= $r; $k++) {
		if ($i > $mid) {
			$arr[$k] = $tmp[$j];
			$j++;
		}elseif ($j > $r) {
			$arr[$k] = $tmp[$i];
			$i++;
		}elseif($tmp[$i] < $tmp[$j]){
			$arr[$k] = $tmp[$i];
			$i++;
		}else{
			$arr[$k] = $tmp[$j];
			$j++;
		}
	}
}


/**
 * [__mergeSort 對區間為[l,r]的元素進行歸併排序]
 * @param  [type] $arr [description]
 * @param  [type] $l   [description]
 * @param  [type] $r   [description]
 * @return [type]      [description]
 */
function __mergeSort(&$arr, $l, $r){
	//此時為一個元素,不需要進行歸併
	if ($l >= $r) {
		return;
	}

	$mid = (int)(($l + $r) / 2);
	// print_r($arr);var_dump($l, $mid, $r);die;
	//對[l, mid]陣列進行歸併
	__mergeSort($arr, $l, $mid);
	//對[mid+1, $r]陣列進行歸併
	__mergeSort($arr, $mid+1, $r);

	merge($arr, $l, $mid, $r);
	
}


function mergeSort(&$arr, $n){

	//對0到n-1的元素今夕歸併
	__mergeSort($arr, 0, $n-1);
}


複製程式碼
$n = 10000;
$arr = generateRandomArray($n, 0, $n);
// $arr = generateNearlyOrderedArray($n, 100);
$copy_arr = $arr;
testSort("mergeSort", "mergeSort", $arr, $n);
testSort("insertSortSeo", "insertSortSeo", $copy_arr, $n);
複製程式碼

執行時間:

mergeSort執行的時間為:0.12523293495178s
insertSortSeo執行的時間為:1.8600959777832s
複製程式碼

當資料近乎有序時候呢?

執行時間:

mergeSort執行的時間為:0.067276000976562s
insertSortSeo執行的時間為:0.048550844192505s
複製程式碼

為什麼還沒有插入排序的效果好呢?有沒有優化的點,有的。

分析:

function __mergeSort(&$arr, $l, $r){
	//此時為一個元素,不需要進行歸併
	if ($l >= $r) {
		return;
	}

	$mid = (int)(($l + $r) / 2);
	// print_r($arr);var_dump($l, $mid, $r);die;
	//對[l, mid]陣列進行歸併
	__mergeSort($arr, $l, $mid);
	//對[mid+1, $r]陣列進行歸併
	__mergeSort($arr, $mid+1, $r);

	merge($arr, $l, $mid, $r);
	
}
複製程式碼

這段程式碼中,我們並沒有考慮陣列有序的情況,無論如何我們都會進行merge,而這是沒有必要的,

優化點1:

function __mergeSort(&$arr, $l, $r){
	//此時為一個元素,不需要進行歸併
	if ($l >= $r) {
		return;
	}

	$mid = (int)(($l + $r) / 2);
	// print_r($arr);var_dump($l, $mid, $r);die;
	//對[l, mid]陣列進行歸併
	__mergeSort($arr, $l, $mid);
	//對[mid+1, $r]陣列進行歸併
	__mergeSort($arr, $mid+1, $r);

	// 優化點1
	if ($arr[$mid] > $arr[$mid+1]) {
		merge($arr, $l, $mid, $r);
	}
}
複製程式碼

執行時間:

mergeSort執行的時間為:0.017416954040527s
insertSortSeo執行的時間為:0.05430006980896s
複製程式碼

優化點2:

我們知道,插入排序對於近乎有序的資料是非常快的,優勢很很大, 那麼可不可以應用到歸併排序上呢,答案是可以的, 當資料規模只有15個元素時候,直接採用插入排序

程式碼改進:

function insertSortCom(&$arr, $l, $r){
   for ($i=$l+1; $i <= $r; $i++) {
        //採用複製的方式
        $tmp = $arr[$i];
        for ($j=$i; $j > $l && $tmp < $arr[$j-1]; $j--) {
            $arr[$j] = $arr[$j-1];
        }
        $arr[$j] = $tmp;
    }
}

function __mergeSort(&$arr, $l, $r){
	//此時為一個元素,不需要進行歸併
	// if ($l >= $r) {
	// 	return;
	// }

	//優化點2:對小規模陣列直接使用插入排序
	if($r - $l < 15 ){
		insertSortCom($arr, $l, $r);
		return;
	}

	$mid = (int)(($l + $r) / 2);
	// print_r($arr);var_dump($l, $mid, $r);die;
	//對[l, mid]陣列進行歸併
	__mergeSort($arr, $l, $mid);
	//對[mid+1, $r]陣列進行歸併
	__mergeSort($arr, $mid+1, $r);

	// 優化點1
	if ($arr[$mid] > $arr[$mid+1]) {
		merge($arr, $l, $mid, $r);
	}
}
複製程式碼

執行時間

mergeSort執行的時間為:0.012771844863892s
insertSortSeo執行的時間為:0.050396919250488s
複製程式碼

自底向上的方法(迭代法)

我們知道遞迴是可以用迭代代替的,我們嘗試改為迭代的方法

//迭代法(自底向上)
function mergeSortBU(&$arr, $n){
	//先是1、1合併,然後是2、2合併,再是4、4合併
	for ($sz=1; $sz < $n; $sz += $sz) {
		for($i = 0; $i < $n; $i+=2*$sz){
			merge($arr, $i, $i+$sz-1, min($i+2*$sz-1, $n-1));
		}
	}
}
複製程式碼

執行結果

// //main
$n = 1000;
$arr = generateRandomArray($n, 0, $n);
// $arr = generateNearlyOrderedArray($n, 100);
$copy_arr = $arr;
testSort("mergeSort", "mergeSort", $arr, $n);
testSort("mergeSortBU", "mergeSortBU", $arr, $n);
testSort("insertSortSeo", "insertSortSeo", $copy_arr, $n);
複製程式碼

結果對比

mergeSort執行的時間為:0.0013480186462402s
mergeSortBU執行的時間為:0.0033299922943115s
insertSortSeo執行的時間為:0.022433042526245s
複製程式碼

-------------------------華麗的分割線--------------------

看完的朋友可以點個喜歡/關注,您的支援是對我最大的鼓勵。

個人部落格番茄技術小棧掘金主頁

想了解更多,歡迎關注我的微信公眾號:番茄技術小棧

番茄技術小棧

相關文章