《算法圖解》之二分查找與快速排序

說明:以下內(nèi)容均參考:[美]Aditya Bhargava所著的《算法圖解》

第一種使用的算法:二分查找竞穷。

二分查找的基本思想:每次都將要查找的序列砍掉一半滑沧,從而砍完整個(gè)序列至多需要 \log_2 n 次竞膳,我們說算法的復(fù)雜度是 O (\log n)

注意:復(fù)雜度中的 \log 指的都是 \log_2 敲才。

注意:僅當(dāng)列表是有序列表的時(shí)候孤钦,二分查找才管用。

二分查找算法的代碼表示:

def binary_search(nums, target):
    lo, hi = 0, len(nums)-1
    
    while lo <= hi:
        mid = (lo + hi) // 2 # 必須要取整肴茄,否則數(shù)組的索引會(huì)出錯(cuò)
        if nums[mid] == target:
            return mid
        if nums[mid] < target:
            # 這里的加1及后面的減1是為了避免lo和hi陷入某些數(shù)中出不去(比如lo=5, hi=6)
            lo = mid + 1 
        else:
            hi = mid - 1
    return None

算法時(shí)間復(fù)雜度中的“線性時(shí)間”的意思是:算法的運(yùn)行時(shí)間與序列的長(zhǎng)度相同晌畅,序列有多長(zhǎng),運(yùn)行時(shí)間就會(huì)有多長(zhǎng)寡痰。其時(shí)間復(fù)雜度表示為 O(n) 抗楔。

二分查找的運(yùn)行時(shí)間為對(duì)數(shù)時(shí)間(\log 時(shí)間)。其時(shí)間復(fù)雜度表示為 O(\log n) 拦坠。

計(jì)算算法時(shí)間復(fù)雜度的目的在于知道算法運(yùn)行時(shí)間的增速连躏,即當(dāng)序列的長(zhǎng)度增加時(shí),算法的運(yùn)行時(shí)間額外增長(zhǎng)得有多快贞滨。例如反粥,當(dāng)序列長(zhǎng)度由一個(gè)較小的值變?yōu)橐粋€(gè)較大的值時(shí),線性時(shí)間復(fù)雜度的額外運(yùn)行時(shí)間增加的非常多,而對(duì)數(shù)時(shí)間額外增加的非常少(幾乎不受序列長(zhǎng)度的影響)才顿,而且序列長(zhǎng)度越長(zhǎng),這種差別就越大尤蒿。因此時(shí)間復(fù)雜度在解決實(shí)際問題的過程中發(fā)揮著非常重要的作用郑气。

O 表示法指出了算法需要執(zhí)行的操作數(shù)(O 可以理解為“Operation”),它指出了算法運(yùn)行時(shí)間的增速腰池,實(shí)際上計(jì)算的是操作數(shù)尾组。(算法的速度指的并非時(shí)間,而是操作數(shù)的增速示弓。在談?wù)撍惴ǖ乃俣葧r(shí)讳侨,我們說的是隨著輸入的增加,其運(yùn)行時(shí)間將以什么樣的速度增加奏属。)

O 表示法實(shí)際上指出的是最壞情況下的運(yùn)行時(shí)間跨跨,它規(guī)定了算法運(yùn)行時(shí)間的上限(比如,假設(shè)一個(gè)算法的運(yùn)行時(shí)間是 O(\log n) 囱皿,此時(shí)我們說這個(gè)算法的運(yùn)行時(shí)間不可能超過 O(\log n) )勇婴。

常見的五大時(shí)間復(fù)雜度:

時(shí)間復(fù)雜度 運(yùn)行時(shí)間 例子
O(\log n) 二分查找
O(n) 較快 簡(jiǎn)單查找
O(n \log n) 快速排序
O(n^2) 較慢 選擇排序
O(n!) 非常慢 旅行商問題

常見大O運(yùn)行時(shí)間的圖示如圖1所示:

圖1:常見的大O運(yùn)行時(shí)間

排序算法的重要性(或者說為什么要學(xué)排序?為什么有那么多的排序算法嘱腥?):因?yàn)楹芏嗨惴▋H在數(shù)據(jù)經(jīng)過排序后才管用(比如二分查找)耕渴。

計(jì)算機(jī)內(nèi)存的工作原理:(計(jì)算機(jī)就像是很多抽屜的集合體,每個(gè)抽屜都有地址齿兔。)當(dāng)我們需要將數(shù)據(jù)存儲(chǔ)到內(nèi)存時(shí)橱脸,便會(huì)請(qǐng)求計(jì)算機(jī)提供存儲(chǔ)空間,然后計(jì)算機(jī)給我們一個(gè)地址分苇。當(dāng)需要存儲(chǔ)多項(xiàng)數(shù)據(jù)時(shí)添诉,有兩種基本方式——數(shù)組和鏈表。

數(shù)組中的元素在內(nèi)存中是相連的组砚。數(shù)組的這個(gè)特點(diǎn)正是導(dǎo)致數(shù)組在添加新元素時(shí)處理速度慢的根本原因:若數(shù)組原來所在的地址對(duì)應(yīng)的內(nèi)存后面沒有多余的內(nèi)存可供新元素加入吻商,那么計(jì)算機(jī)就要為這個(gè)數(shù)組分配一個(gè)新的地址,以保證所有的元素都能緊密相連地放在內(nèi)存中糟红,此時(shí)艾帐,數(shù)組中原先已有的元素就要全部被搬到這個(gè)新的地址中。同理盆偿,若再向數(shù)組中添加新元素時(shí)柒爸,若原有內(nèi)存還不夠,則又要重新分配新地址并搬移原來的所有元素......因此事扭,數(shù)組在動(dòng)態(tài)添加新元素時(shí)會(huì)因?yàn)閮?nèi)存原因而嚴(yán)重影響速度捎稚。 解決這種問題的一種方法是:預(yù)先為數(shù)組分配足夠的內(nèi)存。但是這種方法有一個(gè)缺點(diǎn):不知道要事先分配多少內(nèi)存。如果分配多了則會(huì)導(dǎo)致內(nèi)存浪費(fèi)今野,分配少了就又要重新分配地址葡公。

徹底解決這個(gè)問題的一種方法是鏈表。鏈表中的元素可存儲(chǔ)在內(nèi)存的任何地方:鏈表的每個(gè)元素都存儲(chǔ)了下一個(gè)元素的地址条霜,從而使一系列隨機(jī)的內(nèi)存地址串在一起催什。在鏈表中添加新元素很容易:只需將其放入內(nèi)存,并修改它前面的那個(gè)元素指向的地址宰睡。

數(shù)組和鏈表是優(yōu)勢(shì)互補(bǔ)的:鏈表的優(yōu)勢(shì)在插入元素方面(當(dāng)插入一個(gè)新元素時(shí)很方便)蒲凶,但鏈表的查找就顯得很不方便(當(dāng)我們要查找鏈表中的某個(gè)元素時(shí),我們是無法直接找到該元素的位置的拆内,只能從鏈表的頭結(jié)點(diǎn)開始旋圆,依次訪問下一個(gè)節(jié)點(diǎn),直到找到要找的元素麸恍,因此鏈表的查找會(huì)相當(dāng)費(fèi)時(shí))灵巧; 而恰恰相反,數(shù)組的優(yōu)勢(shì)就在于查找元素(可以迅速地找到數(shù)組的任何元素)或南,但其缺點(diǎn)是插入元素較慢孩等。

當(dāng)需要在中間插入元素時(shí),鏈表是更好的選擇采够。當(dāng)需要?jiǎng)h除元素時(shí)肄方,鏈表也是更好的選擇,因?yàn)橹恍枰薷那耙粋€(gè)元素指向的地址即可蹬癌。而使用數(shù)組時(shí)权她,刪除元素后,必須將后面的元素都向前移逝薪。

鏈表僅支持順序訪問(即從第一個(gè)元素開始逐個(gè)地讀取元素)隅要,而數(shù)組同時(shí)支持順序訪問和隨機(jī)訪問,因此數(shù)組的讀取速度更快董济。實(shí)際情況大多要求能夠隨機(jī)訪問步清,因此數(shù)組用的很多。

數(shù)組和鏈表操作的運(yùn)行時(shí)間如下:

操作 數(shù)組 鏈表
插入 O(n) O(1)
讀取 O(1) O(n)
刪除 O(n) O(1)

選擇排序算法:

算法名稱 選擇排序
算法用途 排序
算法描述 首先遍歷列表虏肾,從中找出元素的最大值廓啊,然后將其添加到一個(gè)新列表中;然后再次遍歷剩余的列表封豪,找出元素第二大的值谴轮,將其按順序添加到新列表中;......直到所有元素被排成一個(gè)新的序列
時(shí)間復(fù)雜度 每次遍歷的時(shí)間復(fù)雜度依次為:O(n)吹埠、O(n-1)第步、...疮装、O(1),總共需要遍歷 n 次粘都,因此總體時(shí)間復(fù)雜度為:O(n^2)

使用遞歸的好處是可以讓程序更容易理解廓推;使用循環(huán)的好處是程序的性能可能更高。

每個(gè)遞歸函數(shù)都有兩部分:遞歸基和遞歸條件翩隧。遞歸基指的是函數(shù)什么時(shí)候不再調(diào)用自己受啥,遞歸條件是指函數(shù)調(diào)用自己。每個(gè)遞歸函數(shù)必須要有遞歸基鸽心,否則會(huì)形成死循環(huán)。

棧的彈出指的是刪除并讀取居暖。

棧的基本操作:壓入(在棧頂添加一個(gè)新的元素)和彈出(刪除并讀取棧頂元素)顽频。

每當(dāng)程序調(diào)用一個(gè)函數(shù)時(shí),計(jì)算機(jī)都將為該函數(shù)調(diào)用分配一塊內(nèi)存太闺,而且該函數(shù)調(diào)用涉及到的所有變量的鍵和值也都將存儲(chǔ)到內(nèi)存中糯景; 如果在這個(gè)函數(shù)里又有新的函數(shù)被調(diào)用,則先前那個(gè)函數(shù)會(huì)成為棧底省骂,然后新來的這個(gè)函數(shù)被壓入棧頂蟀淮,并在棧頂保存新函數(shù)所涉及變量的鍵和值;如果又嵌套的有別的函數(shù)被調(diào)用钞澳,則重復(fù)此過程怠惶; 當(dāng)棧頂?shù)暮瘮?shù)執(zhí)行完時(shí),計(jì)算機(jī)會(huì)將其從棧頂彈出(讀取并刪除)(每一次的return都相當(dāng)于是彈出操作)轧粟,然后前一個(gè)函數(shù)成為新的棧頂策治,當(dāng)其執(zhí)行完后再將其從棧頂彈出;如此直到棧被彈空兰吟,則所有函數(shù)執(zhí)行完畢通惫,程序返回最終結(jié)果。

棧的缺點(diǎn):存儲(chǔ)詳盡的信息可能占用大量的內(nèi)存混蔼。每個(gè)函數(shù)調(diào)用都要占用一定的內(nèi)存履腋,如果棧很高,則意味著計(jì)算機(jī)存儲(chǔ)了大量函數(shù)調(diào)用的信息惭嚣。此時(shí)遵湖,可以考慮將遞歸換成循環(huán)來實(shí)現(xiàn),或者使用尾遞歸(但并非所有的語言都支持尾遞歸)料按。

分而治之是一種通用的問題解決方法奄侠,它隸屬于遞歸式問題解決方法。當(dāng)面對(duì)一個(gè)問題束手無策時(shí)载矿,不妨考慮一下分而治之垄潮。

使用分而治之解決問題的過程包括兩個(gè)步驟:

(1)找出遞歸基烹卒,這個(gè)遞歸基必須盡可能簡(jiǎn)單;

(2)不斷將問題分解(或者說縮小規(guī)模)弯洗,直到符合遞歸基的要求旅急。

分治策略的關(guān)鍵是如何縮小問題的規(guī)模。

分而治之其實(shí)并不是解決問題的算法牡整,而是一種解決問題的思路藐吮。

快速排序使用了分治策略。

一個(gè) list A 和一個(gè)空的 list 相加逃贝,得到的結(jié)果仍為 list A:

[1, 2, 3] + [ ] = [1, 2, 3]

快速排序的代碼如下:

def quicksort(nums):
    if len(nums) < 2:
        return nums # 這是遞歸基:空的或只包含一個(gè)元素的數(shù)組是有序的
    else:
        pivot = nums[0] # 從nums中取出一個(gè)元素當(dāng)做比較的基準(zhǔn)
        # 接下來將原數(shù)組分成兩部分谣辞,一部分是小于pivot的,另一部分是大于pivot的
        # smaller和bigger至少是空列表
        smaller = [i for i in nums[1:] if i <= pivot]
        bigger = [i for i in nums[1:] if i > pivot]
        return quicksort(smaller) + [pivot] + quicksort(bigger)

快速排序是最快的排序算法之一沐扳,其平均時(shí)間復(fù)雜度為 O(n\log n) 泥从,但在最壞情況下,其時(shí)間復(fù)雜度和選擇排序一樣沪摄,均為 O(n^2) 躯嫉。

快速排序的性能嚴(yán)重依賴于每次選擇的基準(zhǔn)值 pivot。在最好情況下杨拐,快速排序的時(shí)間復(fù)雜度為 O(n\log n) 祈餐,這發(fā)生在每次選擇的基準(zhǔn)值都能將余下的數(shù)組二等分,這樣只需要 \log n 次就能將整個(gè)數(shù)組全部分完;而每次的排序操作耗時(shí)均為 O(n) ,因此此時(shí)的時(shí)間復(fù)雜度為 O(\log n ) \times O(n) = O(n\log n)沪斟; 而在最壞的情況下型雳,快速排序的時(shí)間復(fù)雜度將達(dá)到 O(n^2) ,這發(fā)生在每次選擇的基準(zhǔn)值都是剩余數(shù)組的最小值或最大值,這樣每次只能減少數(shù)組的一個(gè)元素。如此一來,將需要 n 次才能將整個(gè)數(shù)組的長(zhǎng)度減為0芭逝,非常耗時(shí),而每次排序的時(shí)間復(fù)雜度均為 O(n) 渊胸,因此這種情況下的整體時(shí)間復(fù)雜度為 O(n) \times O(n) = O(n^2) 旬盯。對(duì)這一解釋的兩個(gè)很形象的圖示如圖 2 - 1 和圖 2 - 2 所示。

圖 2 - 1:最壞情況下的時(shí)間復(fù)雜度:O(n^2)

圖 2 - 2:最好情況下的時(shí)間復(fù)雜度:O(n\log n)

字典的創(chuàng)建翎猛、添加與讀扰趾病:

# 創(chuàng)建一個(gè)字典(以下兩種方式等效)
book = dict()
book = {}

# 添加鍵值對(duì)
book['milk'] = 1.49
book['banana'] = 2.37

# 查找字典中的內(nèi)容
# 字典的查找時(shí)間是O(1)時(shí)間
print(book['banana'])

# 用get方法可以獲取字典中的內(nèi)容
re = book.get('milk') # get中傳入的是鍵,是string類型的值

字典的一種應(yīng)用案例是緩存切厘。緩存是一種常用的加速方式萨咳,所有大型網(wǎng)站都使用緩存,緩存的數(shù)據(jù)存儲(chǔ)在散列表中疫稿。當(dāng)需要提取緩存中的內(nèi)容時(shí)培他,需要將頁面的URL映射到頁面數(shù)據(jù)鹃两。服務(wù)器緩存的一種代碼示例如下所示:

cache = {}

def get_page(url):
    if cache.get(url): # 這里用get而不是直接用cache[url],是因?yàn)槲覀兩胁淮_定cache中有沒有url
        return cache[url] # 返回緩存的數(shù)據(jù)
    else:
        data = get_data_from_server(url) # 如果url不在cache中舀凛,則讓服務(wù)器生成這個(gè)url對(duì)應(yīng)的頁面
        cache[url] = data # 將服務(wù)器生成的內(nèi)容保存到緩存中以便后續(xù)查找時(shí)無需重復(fù)生成
        return data

字典內(nèi)部的工作機(jī)理:

散列函數(shù)負(fù)責(zé)將不同鍵所對(duì)應(yīng)的值映射到不同的位置俊扳,但若不同鍵所對(duì)應(yīng)的值映射到了同一個(gè)位置,就會(huì)引發(fā)沖突猛遍。

解決沖突的一種方法是:在沖突的位置處存儲(chǔ)一個(gè)鏈表馋记。但這里邊存在的問題是:這些存儲(chǔ)的鏈表不能過長(zhǎng),否則字典的速度將急劇下降懊烤。因此這對(duì)散列函數(shù)提出了很高的要求:散列函數(shù)要盡可能地將鍵均勻地映射到字典的不同位置梯醒,即要盡量避免沖突的發(fā)生。

在平均情況下腌紧,字典執(zhí)行各種操作的時(shí)間都為 O(1) 冤馏。O(1) 被稱為常量時(shí)間,但常量時(shí)間并不意味著馬上寄啼,而是說不管散列表多大,所需的時(shí)間都相同代箭。

但在最壞情況下墩划,字典的所有操作(包括查找、插入和刪除)運(yùn)行時(shí)間均為 O(n) 嗡综。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末乙帮,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子极景,更是在濱河造成了極大的恐慌察净,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,036評(píng)論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件盼樟,死亡現(xiàn)場(chǎng)離奇詭異氢卡,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)晨缴,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,046評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門译秦,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人击碗,你說我怎么就攤上這事筑悴。” “怎么了稍途?”我有些...
    開封第一講書人閱讀 164,411評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵阁吝,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我械拍,道長(zhǎng)突勇,這世上最難降的妖魔是什么装盯? 我笑而不...
    開封第一講書人閱讀 58,622評(píng)論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮与境,結(jié)果婚禮上验夯,老公的妹妹穿的比我還像新娘。我一直安慰自己摔刁,他們只是感情好挥转,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,661評(píng)論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著共屈,像睡著了一般绑谣。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上拗引,一...
    開封第一講書人閱讀 51,521評(píng)論 1 304
  • 那天借宵,我揣著相機(jī)與錄音,去河邊找鬼矾削。 笑死壤玫,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的哼凯。 我是一名探鬼主播欲间,決...
    沈念sama閱讀 40,288評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼断部!你這毒婦竟也來了猎贴?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,200評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤蝴光,失蹤者是張志新(化名)和其女友劉穎她渴,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體蔑祟,經(jīng)...
    沈念sama閱讀 45,644評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡趁耗,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,837評(píng)論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了疆虚。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片对粪。...
    茶點(diǎn)故事閱讀 39,953評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖装蓬,靈堂內(nèi)的尸體忽然破棺而出著拭,到底是詐尸還是另有隱情,我是刑警寧澤牍帚,帶...
    沈念sama閱讀 35,673評(píng)論 5 346
  • 正文 年R本政府宣布儡遮,位于F島的核電站,受9級(jí)特大地震影響暗赶,放射性物質(zhì)發(fā)生泄漏鄙币。R本人自食惡果不足惜肃叶,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,281評(píng)論 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望十嘿。 院中可真熱鬧因惭,春花似錦、人聲如沸绩衷。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,889評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽咳燕。三九已至勿决,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間招盲,已是汗流浹背低缩。 一陣腳步聲響...
    開封第一講書人閱讀 33,011評(píng)論 1 269
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留曹货,地道東北人咆繁。 一個(gè)月前我還...
    沈念sama閱讀 48,119評(píng)論 3 370
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像顶籽,于是被迫代替她去往敵國(guó)和親么介。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,901評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容

  • 一些概念 數(shù)據(jù)結(jié)構(gòu)就是研究數(shù)據(jù)的邏輯結(jié)構(gòu)和物理結(jié)構(gòu)以及它們之間相互關(guān)系蜕衡,并對(duì)這種結(jié)構(gòu)定義相應(yīng)的運(yùn)算,而且確保經(jīng)過這...
    Winterfell_Z閱讀 5,802評(píng)論 0 13
  • Swift1> Swift和OC的區(qū)別1.1> Swift沒有地址/指針的概念1.2> 泛型1.3> 類型嚴(yán)謹(jǐn) 對(duì)...
    cosWriter閱讀 11,101評(píng)論 1 32
  • 1 初級(jí)排序算法 排序算法關(guān)注的主要是重新排列數(shù)組元素设拟,其中每個(gè)元素都有一個(gè)主鍵慨仿。排序算法是將所有元素主鍵按某種方...
    深度沉迷學(xué)習(xí)閱讀 1,408評(píng)論 0 1
  • Java8張圖 11、字符串不變性 12纳胧、equals()方法镰吆、hashCode()方法的區(qū)別 13、...
    Miley_MOJIE閱讀 3,707評(píng)論 0 11
  • 逆流成河wsy閱讀 159評(píng)論 0 1