免费视频淫片aa毛片_日韩高清在线亚洲专区vr_日韩大片免费观看视频播放_亚洲欧美国产精品完整版

打開APP
userphoto
未登錄

開通VIP,暢享免費(fèi)電子書等14項(xiàng)超值服

開通VIP
PHP數(shù)據(jù)結(jié)構(gòu)-交換排序:冒泡、快排(有彩蛋)

交換排序:冒泡、快排

上篇文章中我們好好地學(xué)習(xí)了一下插入類相關(guān)的兩個(gè)排序,不過(guò),和交換類的排序?qū)Ρ鹊脑?,它們真的只是弟弟。甚至可以說(shuō),在所有的排序算法中,最出名的兩個(gè)排序都在今天要介紹的交換排序中了。不管是冒泡、還是快排,都是面試中的常見排序算法,常見到什么地步呢?但凡學(xué)習(xí)數(shù)據(jù)結(jié)構(gòu)和算法,甚至是你完全沒(méi)有學(xué)習(xí)過(guò),也多少都會(huì)聽說(shuō)過(guò)這兩個(gè)排序算法。而一些大中型公司更是直接在面試題中指明不要使用這兩種算法來(lái)實(shí)現(xiàn)一些排序的題目,這又是為什么呢?那當(dāng)然也是因?yàn)檫@兩個(gè)算法實(shí)在是太出名了,很多人都隨便就能手寫出來(lái)。

當(dāng)然,不管你面試的公司有什么要求,只要是有志在編程開發(fā)這個(gè)行業(yè)里發(fā)展的同學(xué),冒泡和快排肯定會(huì)是面試中繞不開的一個(gè)坎。我們今天就來(lái)好好地學(xué)習(xí)一下這兩個(gè)排序算法。不過(guò)首先還是要搞明白這個(gè)“交換”指的是什么意思。

上篇文章中的插入排序,指的是直接將數(shù)據(jù)插入到指定的位置。而交換的意思,則是讓兩個(gè)位置的數(shù)據(jù)在進(jìn)行比對(duì)后直接交換。比如我們有 [3, 1, 2] 這樣一個(gè)數(shù)組,需要排列成 [1, 2, 3] 這種形式。那么我們就可以先讓 3 和 1比較,發(fā)現(xiàn) 1 小,于是將 3 和 1 的位置進(jìn)行交換,結(jié)果是 [1, 3, 2] 。然后再讓 3 和 2 比較,發(fā)現(xiàn) 2 小,再交換它們的位置,于是得到結(jié)果為 [1, 2, 3] 的數(shù)組。

當(dāng)然,這個(gè)示例只是簡(jiǎn)單地說(shuō)明了一下交換排序的原理。但萬(wàn)變不離其宗,不管是冒泡還是快排,它們的基本原理和核心思想都是這樣的,讓兩個(gè)數(shù)據(jù)對(duì)比后根據(jù)規(guī)則交換位置。這里其實(shí)從代碼中我們能夠從一個(gè)地方很快地分辨出一段排序代碼是否是交換排序,那就是他們會(huì)有一個(gè)對(duì)于兩個(gè)元素進(jìn)行數(shù)據(jù)交換的過(guò)程,而且往往在普通情況下會(huì)使用一個(gè)中間變量。這個(gè)我們一會(huì)看代碼就可以看到。

冒泡排序

冒泡排序,先從名字來(lái)理解一下,它的意思其實(shí)是讓數(shù)據(jù)像汽水中的泡泡一樣一個(gè)一個(gè)的浮上來(lái)。

直接上代碼了來(lái)看看,代碼其實(shí)非常簡(jiǎn)單。

function BubbleSort($numbers)
{
    $n = count($numbers);

    for ($i = 0; $i < $n - 1; $i++) { // 外層循環(huán) n - 1
        for ($j = 0; $j < $n - $i - 1; $j++) { // 內(nèi)層循環(huán) n - 1 - i
            if ($numbers[$j] > $numbers[$j + 1]) { // 兩兩相比來(lái)交換
                $temp = $numbers[$j + 1];
                $numbers[$j + 1] = $numbers[$j];
                $numbers[$j] = $temp;
            }
        }
    }

    print_r($numbers);
}

BubbleSort($numbers);
// Array
// (
//     [0] => 13
//     [1] => 27
//     [2] => 38
//     [3] => 49
//     [4] => 49
//     [5] => 65
//     [6] => 76
//     [7] => 97
// )

光看代碼自己推演的話其實(shí)還是不太好理解,那么我們就還是使出終極殺器,也就是圖解步驟來(lái)看一下吧!


在代碼中可以看到,我們有兩層循環(huán)。所以這個(gè)圖片中我們也是展示了 i 和 j 的兩層循環(huán)情況。當(dāng)然,限于篇幅,我們只展示了第一次 i 循環(huán)內(nèi)部的 j 循環(huán)情況,也就是 i = 0 時(shí),里面的 j 循環(huán)執(zhí)行的情況。

  • i = 0 是,內(nèi)部的 j < n - 1 - i ,也就是內(nèi)部的 j 要循環(huán)七次。我們直接就看右邊的 j 循環(huán)的步驟。

  • 冒泡排序其實(shí)就是利用 j 和 j + 1 來(lái)對(duì)比兩個(gè)相鄰的元素。從圖中我們就可以看出,每一次 j++ 都是在對(duì)當(dāng)前 j 和下一個(gè) j + 1 的元素進(jìn)行比較。如果當(dāng)前的這個(gè) j 大于 j + 1 的話,就把它們交換位置。

  • 當(dāng) j = 0 時(shí),第 0 個(gè)位置的 49 是大于第 1 個(gè)位置的 38 的,于是 49 和 38 交換了位置。

  • 當(dāng) j = 1 時(shí),位置 1 的 49 和位置 2 的 65 相比,沒(méi)有達(dá)成條件,于是不會(huì)變動(dòng)。同理,j = 2 時(shí)也是對(duì)比的 65 和 97 ,同樣不會(huì)發(fā)生交換。

  • 當(dāng) j = 3 時(shí),97 比 76 要大,于是發(fā)生了交換,97 交換到 j + 1 也就是下標(biāo) 4 的位置。同時(shí),97 也是整個(gè)序列中最大的數(shù),于是后面會(huì)一直交換到這次的 j 循環(huán)結(jié)束。

  • 最終的結(jié)果是 97 這個(gè)最大的數(shù)移動(dòng)到了數(shù)據(jù)的最后一位。也就是說(shuō),最大的數(shù)已經(jīng)放到了整個(gè)序列中的正確的位置上了。

  • 接著內(nèi)層循環(huán)結(jié)束,i++ ,開始第二次 i = 1 的內(nèi)部 j 循環(huán)。這里需要注意的是,為什么我們要用 j < n - 1 - i 呢?因?yàn)槲覀兦懊嬉呀?jīng)完成了一個(gè)最大數(shù)的排序,就是將 97 這個(gè)最大數(shù)放到了最后的位置上。所以在 i++ 的第二次循環(huán)時(shí),我們就要將第二大的數(shù)放在倒數(shù)第二的位置上。這時(shí)的 j 也不需要循環(huán)到最后一位了,只需要循環(huán)到倒數(shù)第二位就可以了。

從上面的分步講解中,我們可以看到,外層的 i 每一次的循環(huán)其實(shí)就是通過(guò)內(nèi)層的 j 循環(huán)來(lái)將一個(gè)最大的數(shù)按順序放到后面的位置上。就像汽水不斷地向上冒泡一樣,它就是傳說(shuō)中的冒泡排序算法概念的由來(lái)。

其實(shí)關(guān)于冒泡排序的算法,還有一個(gè)口決是很多同學(xué)都知道的,也可以幫助我們記憶。

  • 外層循環(huán) N 減一

  • 內(nèi)層循環(huán) N 減一減 I

  • 兩兩相比小靠前(正序)

為什么小的靠前是正序呢?在代碼中,我們 if 條件判斷是的 j > j+1 ,如果成立就交換它們,也就是讓大的數(shù)據(jù)放到了后面,小的數(shù)據(jù)放到了前面,這樣一輪過(guò)后,最大的數(shù)據(jù)放在了最后一位,也就是完成了一個(gè)最大數(shù)據(jù)的位置的確定。如果我們將條件反過(guò)來(lái),也就是 j < j+1 的話,就會(huì)讓最大的數(shù)據(jù)放到最前面,也就是實(shí)現(xiàn)了倒序。是不是很神奇?小伙伴們可以試試哦,就改變一下 if 條件的大于號(hào)就可以了哦。

冒泡的時(shí)間復(fù)雜度其實(shí)很明顯地就能看出來(lái),O(N2)。屬于效率一般但非常好理解的一種算法,而且它是一個(gè)穩(wěn)定的排序算法。

快速排序

冒泡的感覺咋樣?不過(guò)冒泡有個(gè)問(wèn)題,那就是它只能對(duì)相鄰的兩個(gè)數(shù)據(jù)進(jìn)行比較,所以 O(N2) 這個(gè)時(shí)間復(fù)雜度基本也就不包含什么最好最壞的情況了,不管怎么它都得要達(dá)到這個(gè) O(N2) 的水平。

那么有沒(méi)有什么別的方法能夠?qū)γ芭葸M(jìn)行優(yōu)化呢?有大佬就發(fā)明出了優(yōu)化冒泡的一種排序算法啦。那就是快速排序算法。還記得在學(xué)習(xí)查找的時(shí)候我們學(xué)習(xí)過(guò)的二分查找嗎?相對(duì)于線性查找來(lái)說(shuō),二分查找的效率是不是提升了很多。但快速排序的效率提升可達(dá)不到那么高,畢竟排序還是比查找要復(fù)雜些。而且它是在冒泡的基礎(chǔ)上進(jìn)行的改良,同樣也使用了二分的思想,也就是分而治之的一種理念。讓每次的比較不再只是兩個(gè)相鄰的元素一個(gè)一個(gè)地比較。所以它的平均時(shí)間復(fù)雜度可以提升到 O(NlogN) 的級(jí)別。相對(duì)于 O(N2) 來(lái)說(shuō),這個(gè)時(shí)間復(fù)雜度其實(shí)也有了不小的飛躍哦。特別是數(shù)據(jù)量越大的情況下越明顯。

同樣我們先來(lái)看看代碼,然后再來(lái)看圖分析這個(gè)算法。

function QSort(&$arr, $start, $end)
{
    if ($start > $end) {
        return;
    }
    $key = $arr[$start];
    $left = $start;
    $right = $end;
    
    while ($left < $right) {
        // 右邊下標(biāo)確定
        while ($left < $right && $arr[$right] >= $key) {
            $right--;
        }
        // 左邊下標(biāo)確定
        while ($left < $right && $arr[$left] <= $key) {
            $left++;
        }
        if ($left < $right) { // 交換步驟
            $tmp = $arr[$left];
            $arr[$left] = $arr[$right];
            $arr[$right] = $tmp;
        }
    }

    $arr[$start] = $arr[$left];
    $arr[$left] = $key;
    // 遞歸左右兩邊繼續(xù)
    QSort($arr, $start, $right - 1);
    QSort($arr, $right + 1, $end);
}

function QuickSort($numbers)
{
    QSort($numbers, 0, count($numbers) - 1);
    print_r($numbers);
}

QuickSort($numbers);
// Array
// (
//     [0] => 13
//     [1] => 27
//     [2] => 38
//     [3] => 49
//     [4] => 49
//     [5] => 65
//     [6] => 76
//     [7] => 97
// )

有沒(méi)有發(fā)現(xiàn)熟悉的身影?沒(méi)錯(cuò),快速排序使用到了遞歸。這個(gè)遞歸其實(shí)也包含著分治的思想,就像秦國(guó)統(tǒng)一六國(guó)一樣,分而治之。我們將某一個(gè)數(shù)據(jù)放到指定的位置之后再按左右分治的方式來(lái)繼續(xù)其它的數(shù)據(jù)的排序,而不用讓其它的數(shù)據(jù)再對(duì)整個(gè)序列進(jìn)行完整的判斷,從而提高排序的效率。因此,快排的時(shí)間復(fù)雜度相對(duì)冒泡來(lái)說(shuō)就好了很多。


同樣地,它表面上是不停地遞歸,其實(shí)遞歸也是一種循環(huán),我們就可以看出來(lái),它和冒泡一樣其實(shí)是有著兩層循環(huán)的概念的。這里我們也是以第一次的外層循環(huán)為例子來(lái)剖析它的內(nèi)層循環(huán)都做了什么。

  • 首先,我們確定了一個(gè)關(guān)鍵字 key ,這里我們就直接指定第一個(gè)數(shù)據(jù) 49 。然后指定左右兩個(gè)指針,左指針 left 從 0 開始,右指針 right 從最右邊的下標(biāo)開始。

  • 進(jìn)入內(nèi)層循環(huán),條件是 left < right ,也就是左右兩個(gè)指針不能相遇!

  • 開始指針移動(dòng),先從右邊開始,如果 right 指向的數(shù)據(jù)大于等于 key ,right 就進(jìn)行減減操作,否則,指針就停住??梢钥吹?,我們的指針停在了 27 這個(gè)數(shù)據(jù)的位置,也就是倒數(shù)第二個(gè)數(shù)據(jù)這里,第一個(gè)數(shù)據(jù) 49 和我們的 key 值 49 是一樣的,于是 right 就移動(dòng)到倒數(shù)第二個(gè)數(shù)據(jù)了,27 是小于 key 值的。

  • 然后移動(dòng) left 指針,移動(dòng)到符合條件的位置也就是值為 65 的這個(gè)下標(biāo),然后交換 left 和 right 的值。

  • 繼續(xù)后續(xù)的操作,直到 left 和 right 相遇了,這時(shí)退出循環(huán),并在循環(huán)外面再次交換 key 和 left 位置的值。這時(shí),第一個(gè)下標(biāo)的 49 這個(gè)值就已經(jīng)放到了它所確定的位置。也就是說(shuō),這個(gè)值完成了排序。

  • 接著,以這個(gè)完成排序的值為中心,切分左右兩個(gè)序列,繼續(xù)進(jìn)入遞歸排序的過(guò)程,直到所有數(shù)據(jù)完成排序。

看出快速排序和冒泡排序的區(qū)別了吧?快排的每趟排序都會(huì)確定一個(gè)關(guān)鍵字的具體位置,它的比較除了第一次是每個(gè)數(shù)都和 key 兩兩比較之外,其它都是采用分治的思想來(lái)縮小 n 的大小進(jìn)行小范圍的排序的。而且每次的循環(huán)都會(huì)將數(shù)據(jù)按針對(duì) key 值的大小進(jìn)行左右排列,這也是二叉搜索樹的核心思想。這個(gè)內(nèi)容我們的系列文章中沒(méi)有講解,大家可以自行查閱相關(guān)的資料學(xué)習(xí)。

小彩蛋:交換兩個(gè)變量的值

今天學(xué)習(xí)的內(nèi)容中都有一處核心的代碼,就是最開始我們說(shuō)過(guò)的交換兩個(gè)變量值的代碼。

// 冒泡中
$temp = $numbers[$j + 1];
$numbers[$j + 1] = $numbers[$j];
$numbers[$j] = $temp;

// 快排中
$tmp = $arr[$left];
$arr[$left] = $arr[$right];
$arr[$right] = $tmp;

我們都使用到了一個(gè)臨時(shí)變量來(lái)進(jìn)行交換。不過(guò)不少的面試題中經(jīng)常會(huì)看到一種題目就是不使用第三個(gè)變量,也就是這個(gè)臨時(shí)變量來(lái)交換兩個(gè)變量的值。大家有沒(méi)有踫到過(guò)呢?其實(shí)有幾種方案都可以,我們就來(lái)簡(jiǎn)單說(shuō)兩個(gè)。

$a = 1;
$b = 2;
$a += $b; // a = 3
$b = $a - $b; // b = 3 - 2 = 1 
$a = $a - $b; // a = 3 - 1 = 2
echo $a, PHP_EOL; // 2
echo $b, PHP_EOL; // 1

$a = "a";
$b = "b";
$a .= $b; // a = "ab"
$b = str_replace($b, "", $a); // b = str_replace("b", "", "ab") = a
$a = str_replace($b, "", $a);// a = str_replace("a", "", "ab") = b
echo $a, PHP_EOL; // b
echo $b, PHP_EOL; // a

對(duì)于數(shù)字來(lái)說(shuō),直接使用第一段的加減操作就可以完成兩個(gè)變量的交換。而對(duì)于字符串來(lái)說(shuō),就可以使用 str_replace() 來(lái)實(shí)現(xiàn)。其實(shí)它們的思想都是一樣的,先合并到一個(gè)變量上,然后利用減法或者替換來(lái)讓某一個(gè)變量先變成另一個(gè)變量的值。然后再使用相同的方法將另一個(gè)變量的值也轉(zhuǎn)換成功。當(dāng)然,這只是最簡(jiǎn)單最基礎(chǔ)的一種算法,利用 PHP 的一些函數(shù)和特性,我們還可以更方便地實(shí)現(xiàn)這種功能。

$a = 1;
$b = 2;
list($a, $b) = [$b, $a];
echo $a, PHP_EOL; // 2
echo $b, PHP_EOL; // 1

list() 函數(shù)是將一個(gè)數(shù)組中的數(shù)據(jù)分別存入到指定的變量中,而在 PHP 中我們也可以直接 [x, x] 這樣來(lái)定義數(shù)組。所以不使用第三個(gè)臨時(shí)變量來(lái)交換兩個(gè)變量的功能我們只用這一行代碼就搞定了。list(b) = [a] 。這里不點(diǎn)贊可真對(duì)不起這道題咯?。?/p>

總結(jié)

交換排序的這兩種算法相當(dāng)于數(shù)據(jù)結(jié)構(gòu)與算法這門課程的門面擔(dān)當(dāng),但凡要講算法中的排序的,必然會(huì)有它們兩個(gè)的身影。畢竟太經(jīng)典了,不過(guò)我們可是先學(xué)了兩個(gè)插入類的排序進(jìn)行過(guò)了熱身才來(lái)學(xué)習(xí)這兩個(gè)經(jīng)典算法的,相信大家進(jìn)行對(duì)比之后就能更深入地理解這些算法的神奇和不同。

測(cè)試代碼:

https://github.com/zhangyue0503/Data-structure-and-algorithm/blob/master/7.排序/source/7.2交換排序:冒泡、快排.php

參考文檔:

本文示例選自 《數(shù)據(jù)結(jié)構(gòu)》第二版,嚴(yán)蔚敏

《數(shù)據(jù)結(jié)構(gòu)》第二版,陳越

本站僅提供存儲(chǔ)服務(wù),所有內(nèi)容均由用戶發(fā)布,如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,請(qǐng)點(diǎn)擊舉報(bào)。
打開APP,閱讀全文并永久保存 查看更多類似文章
猜你喜歡
類似文章
算法創(chuàng)作 | 冒泡排序問(wèn)題解決方法
冒泡排序,經(jīng)典的排序算法
如何優(yōu)化EXCEL vba代碼?
PHP foreach的兩種用法 as $key => $value
PHP 對(duì)數(shù)組使用 自然算法 進(jìn)行排序 natsort 與 natcasesort 函數(shù)
php四種排序算法代碼
更多類似文章 >>
生活服務(wù)
分享 收藏 導(dǎo)長(zhǎng)圖 關(guān)注 下載文章
綁定賬號(hào)成功
后續(xù)可登錄賬號(hào)暢享VIP特權(quán)!
如果VIP功能使用有故障,
可點(diǎn)擊這里聯(lián)系客服!

聯(lián)系客服