排序筆記

一、排序簡介

我們通常所說的排序算法往往指的是內部排序算法,即數據記錄在內存中進行排序。

排序算法大體可分為兩種:
一種是比較排序,時間復雜度O(nlogn) ~ O(n^2),主要有:冒泡排序,選擇排序,插入排序,歸并排序,堆排序,快速排序等。
另一種是非比較排序,時間復雜度可以達到O(n),主要有:計數排序,基數排序,桶排序等。

穩定性:冒泡直接歸并。

二、冒泡排序

參考冒泡排序

冒泡排序算法的運作如下:

  • 比較相鄰的元素,如果前一個比后一個大,就把它們兩個調換位置。
  • 對每一對相鄰元素作同樣的工作,從開始第一對到結尾的最后一對。這步做完后,最后的元素會是最大的數。
  • 針對所有的元素重復以上的步驟,除了最后一個。
  • 持續每次對越來越少的元素重復上面的步驟,直到沒有任何一對數字需要比較。

對于圖片上的直觀理解,建議看冒泡排序中代碼后面的圖片。

需要注意的地方:

  • 1、因為是需要比較兩個元素,所以在索引時用到了alist[i+1]的情況,因此在為了不讓index超出范圍,應該讓for循環中的passnum-1。
  • 2、每一次循環,最大的元素都已經排到列表最后了,下一次循環就可以減短列表長度了,因此在循環結束以后需要加上passnum -= 1。

代碼實現:

def bubbleSort(alist):
    #構造一個倒序列表,從而限制了每次遍歷一次以后的循環長度
    passnum = len(alist)
    while passnum > 0:
        for i in range(passnum-1):
            if alist[i]>alist[i+1]:
                temp = alist[i]
                alist[i] = alist[i+1]
                alist[i+1] = temp
        passnum -= 1

alist = [54,26,93,17,77,31,44,55,20]
bubbleSort(alist)
print(alist)

冒泡的改進:雞尾酒排序短路冒泡排序

三、選擇排序

選擇排序也是一種簡單直觀的排序算法。(以從小到大的排序為例。)
1、初始時在序列中找到最小元素,放到序列的起始位置作為已排序序列;
2、然后,再從剩余未排序元素中繼續尋找最小元素,放到已排序序列的末尾。以此類推,直到所有元素均排序完畢。

具體操作圖片見 選擇排序

選擇排序與冒泡排序的區別:

  • 冒泡排序通過依次交換相鄰兩個順序不合法的元素位置,從而將當前最小元素放到合適的位置
  • 選擇排序每遍歷一次都記住了當前最?。ù螅┰氐奈恢?/strong>,最后僅需一次交換操作即可將其放到合適的位置。

由于選擇排序需要記住的是當前最大元素的位置,因此需要設置一個index_max的變量并令初始值為0 ,接下來就要比較每個元素,循環一次,就能得到最大值的index了。
接下來就是交換元素的操作。
另外,這里的for循環中passnum應該保持不變。

代碼實現:

def selectionSort(alist):
    #構造一個倒序列表,從而限制了每次遍歷一次以后的循環長度
    passnum = len(alist)
    while passnum > 0:
        index_max = 0
        for i in range(passnum):
            if alist[i]>alist[index_max]:
                index_max = i
        temp = alist[passnum-1]  
        alist[passnum-1] = alist[index_max]
        alist[index_max] = temp
        passnum -= 1

alist = [54,26,93,17,77,31,44,55,20]
selectSort(alist)
print(alist)

四、插入排序

插入排序是一種簡單直觀的排序算法。它的工作原理非常類似于我們抓撲克牌。
如下圖:

對于一個列表[5,4,2,10,7],在用插入排序到達[2,4,5,10,7]的結果時,對于最后一個元素7,從后向前進行比較,比10小,就與10交換位置,直到前面一位的元素比7小為止。

注意,在從后向前掃描的過程中,需要不斷地交換兩個元素的位置。

從圖像上直觀理解插入排序

偽代碼:

  • 從第一個元素開始,該元素可以認為已經被排序
  • 取出下一個元素,在已經排序的元素序列中從后向前掃描
  • 如果該元素(已排序)大于新元素,將該元素移到下一位置
  • 重復步驟3,直到找到已排序的元素小于或者等于新元素的位置
  • 將新元素插入到該位置后
  • 重復步驟2~5

代碼:
按照偽代碼,此處需要加兩個循環:

  • 1、一個是for循環,按照插入排序的思想,需要對列表中每個元素執行一次插入操作,從左到右。
  • 2、一個是while循環,執行比較與換位操作。
    若是前一個元素nums[cur-1]大于當前元素nums[cur],則二者互換位置
    1、while循環結束條件:
    當cur指向的元素為0即列表開頭時,停止循環
    當cur元素小于等于第cur-1的元素時,目的已經達到,執行break語句。
    2、while循環改變語句:
    每循環一次,cur指針就要減1,與前面的前面的元素進行比較

代碼實現:

def Insertionsort(nums):
    for i in range(len(nums)):
        cur = i
        while cur >= 1:
            if nums[cur-1] > nums[cur]:
                temp = nums[cur - 1]
                nums[cur - 1] = nums[cur]
                nums[cur] = temp
                cur -= 1
            else:
                break
    return nums

print(Insertionsort([6, 5, 4, 1, 8, 7, 2, 4 ]))

五、希爾排序

希爾排序,也叫遞減增量排序,是插入排序的一種更高效的改進版本。

希爾排序是基于插入排序的以下兩點性質而提出改進方法的:

  • 插入排序在對幾乎已經排好序的數據操作時,效率高,即可以達到線性排序的效率
  • 但插入排序一般來說是低效的,因為插入排序每次只能將數據移動一位

于是,我們提出一個想法,(針對第二點)對于每個元素,每次不再只移動一位,而是“大踏步”的移動。(針對第一點)經過“大踏步”的移動以后,順序相對好很多,此時再進行插入排序,效果會好很多。

針對上面你的想法,我們再具體的提出一些改進。
比如,對于一個有9個元素的list,我們取gap為3,先對index為0,3,6的元素進行插入排序,再對1,4,7的子列表插入排序,再對2,5,8的子列表進行插入排序。

經過對這三個子列表進行排序以后,我們發現此時的列表相對好了很多:

此時再進行插入排序,只需要再經過三次的替換就行了:

另外我們注意到,gap == 子列表數。比如,我們設定gap為3,那么子列表的數目也就是3。怎么理解呢,也很好理解:從第0位元素出發,3為gap向右走到頭是sublist1,同理從第1位出發, 第2位出發,可以得到sublist2,sublist3。這三個子列表就已經把整個list給遍歷完了。

也就是說,對于一個gap,我們會得到gap個子列表,而這gap個子列表的起始元素列表是

start_list = [x for i in range(gap)]

我們可以通過上面的理解,得到要進行插入排序的每個子列表的起始元素。

我們再定義一個插入排序函數,就是上一節的插入排序代碼,只不過gap不再是1了。傳入列表nums,子列表的起始元素start,gap,我們便可以對這個子列表進行插入排序。

而這里的希爾排序,我們不是固定了gap,我們先采用最大的gap(即len(nums)//2),然后依次將gap除以2,直到gap為1進行最后的插入排序。比如上面例子中,我們采用的是長度為9 的list,那么我們先用gap為4,此時就有四個子列表,我們對這四個子列表調用插入排序的函數。下次gap為2,就有2個子列表,再調用插入排序的函數。。。

代碼思路

  • 所以需要一個while循環,循環對不同的gap進行分列表以及排序操作,循環結束條件為gap等于0.
  • while里再需要一個for循環,按照上面所說對gap子列表調用插入排序函數。
  • 定義一個傳入start,gap,nums,能進行插入排序的函數。

代碼實現:

def shellSort(nums):
    #gap初始化為最大的gap,然后在while循環中不斷整除2,減小gap
    gap = len(nums)//2
    while gap > 0:
        #對gap個子列表調用插入排序函數
        for startposition in range(gap):
            gapInsertionSort(nums,startposition,gap)
        print("After increments of size",gap,
                                   "The list is",nums)
        gap = gap // 2

#輸入nums,起始位置,gap,就可以選出子列表,并且進行插入排序
def gapInsertionSort(nums,start,gap):
    for i in range(start,len(nums),gap):
        cur = i
        while cur >= gap:
            if nums[cur - gap] > nums[cur]:
                temp = nums[cur - gap]
                nums[cur - gap] = nums[cur]
                nums[cur] = temp
                cur -= gap
            else:
                break

nums = [54,26,93,17,77,31,44,55,20]
shellSort(nums)
print(nums)

六、歸并排序

參考歸并排序

最易于理解的白話:首先考慮下如何將將二個有序數列合并

  • 1、這個非常簡單,只要從比較二個數列的第一個數,誰小就先取誰,取了后就在對應數列中刪除這個數。
  • 2、然后再進行比較,如果有數列為空,那直接將另一個數列的數據依次取出即可。

比如,13跟24678合并。
1跟2比較,1小于2,那么list.append(1)。
3跟2比較,2小于3,那么list.append(2)
3跟4比較,3小于4,那么list.append(3)
left數列已經為空,那么就把right的數列都append到list中。

那么歸并排序呢,一樣的道理。但是不一樣的地方就是,一開始不是兩個有序list,而是是一整個無序list,為了滿足“兩個有序”的要求,我們分兩步:

  • 1、先把list分成left和right兩個無序的部分
  • 2、把這兩個無序的部分,調用函數排成兩個有序的部分
    這里的兩個無序的部分怎么排序成有序的部分呢。就是遞歸調用歸并排序函數了。
  • 3、對兩個有序的部分,進行上面的merge操作即可。

其中遞歸的地方就是,上面的第二步中,上面的

代碼思路

  • 寫一個對兩個有序list進行歸并的函數merge。傳入兩個有序list,返回一個合并好的list。
  • 寫一個遞歸調用自身的歸并排序函數mergesort,傳入一個無序list,調用merge,返回一個有序list。內容如下
    1、把無序列表分成兩個無序列表
    2、對上面兩個無序列表遞歸調用歸并排序函數mergesort,能夠返回兩個有序的子列表。
    3、對上一步返回的兩個有序子列表,調用merge函數。

把一個list一路遞歸到每個list長度為1的時候,返回兩個長度為2的有序list,合并后再返回兩個長度為4的有序list,再合并。。一直到返回len(nums)的list。

代碼來自知乎A2N
運行過程

具體細節圖片參考歸并排序

代碼實現:

def MergeSort(lists):
    #遞歸結束條件,list小于等于1
    if len(lists) <= 1:
        return lists
    #把無序list分成兩個部分
    num = int( len(lists)/2 )
    #對這兩個無序list,遞歸調用歸并排序函數
    left = MergeSort(lists[:num])
    right = MergeSort(lists[num:])
    #對于返回的兩個有序list,調用merge函數
    return Merge(left, right)


#按照合并兩個有序list的論述,定義一個合并函數
def Merge(left,right):
    r, l=0, 0
    result=[]
    while l<len(left) and r<len(right):
        if left[l] < right[r]:
            result.append(left[l])
            l += 1
        else:
            result.append(right[r])
            r += 1
    result += right[r:]
    result += left[l:]
    return result
print(MergeSort([1, 2, 3, 4, 5, 6, 7, 90, 21, 23, 45]))

七、堆排序

對于堆的理解,參考樹4,二叉樹的特例——堆

三、快速排序

3.1、大致理解快速排序的方針

3.1.1、以中間數為基準,先把一個list分成兩個大小兩個區域

對于一個無序list,取index為0的元素作為中間數,然后執行分區函數,讓比中間數小的點都在中間數左邊,比中間數大的點都在中間數右邊。此時得到了一個看似有序其實無序的list:

  • 有序體現在此時list分成了兩個區域,左邊的都比中間數小,右邊的都比中間數大。
  • 無序體現在,而這兩個區域,又都是無序的list

3.1.2、對上述的兩個區域,遞歸調用快速排序

我們對這兩個無序的list再次調用分區函數,此時會得到四個看似有序而又無序的list,接著調用分區函數,直到最終list長度為2時,再調用一次分區函數,那么一定是左邊有序并且小于右邊,因此此時所有的子列表都是有序的,那么此時一整個list也就是一個有序的list了。

3.2、快速排序的細節問題

3.2.1、分區函數怎么分區

我們取index為0的元素為中間數,left指針代表其指向的元素應該在中間數左邊,即小于中間數;right代表其指向的元素應該在中間數右邊,大于中間數。而實際情況不會這么完美,于是我們進行下面這樣的操作,如下圖:

圖1

無序列表為nums,對于我們要執行分區函數的列表調用分區函數:

  • 一開始我們選54作為中間節點,定義left指針指向index為1的元素,right指向列表的最后一個元素。左右指針開始匯合;
  • 一開始左指針指向元素為26,小于54,說明這個元素的位置是正確的,則左指針加1向右移動,指向96時,這個元素位置是錯誤的,暫時停止left的移動
  • 同理,我們移動右指針,直到找到錯誤的元素,為20
  • 對這兩個位置錯誤的元素,調換位置,然后再接著移動左右指針。
圖2

移動過程中發現位置錯誤的元素,繼續調換位置。
直到最后這種情況:

圖3

此時左指針指向77停止,右指針指向31停止,但是此時左指針大于右指針了,說明他倆指向了兩人已經工作過的區域了,此時不用再調換位置了。直接替換右指針指向元素與中間數,就能得到一個,已經分好區域的list了。左右兩個區域。

3.2.2、怎么調用遞歸函數使得能夠繼續分區

上面我們其實會得到兩個小的無序的區域,以什么劃分這兩個區域呢(注意中間數不用再摻和進去了),就是中間元素的index啊。于是:

  • 左邊的無序區域為nums[first:index-1]
  • 右邊的無序區域為nums[index+1,last]
    于是對這兩個區域再調用遞歸函數即可。

3.3、代碼思路:

3.3.1、定義快排函數quickSort()

根據上面所說,遞歸調用的函數,需要傳入三個參數,一個是列表nums,還有兩個是用來劃分需要快排區域的參數first和last。
而一開始我們只能傳入一個參數就是列表nums。
因此我們定義一個輔助函數quickSortHelper(),既能用來遞歸調用快排操作,又能傳入三個參數。
初始化中,傳入的三個參數分別是nums,first = 0,last = len(nums) - 1

3.3.2、定義遞歸調用函數partition()

此函數傳入三個參數,列表nums,以及用來劃分要對列表進行快排操作的區域指針,first和last
執行此函數后,函數會將要分區的列表區域進行分區。

  • 1、遞歸結束條件是first小于last
  • 2、函數改變條件,每次傳入的需要排序的區域指針都會不斷改變,直到first大于last截止
    函數里面調用一次分區函數,返回中間數的index,用來改變下次調用遞歸函數的first與last。
    其中左半部分區域為first,index-1.右半部分為index+1,last
  • 遞歸調用,上一步,得到遞歸調用的區域,直接遞歸調用兩次快排函數即可。

3.3.3、定義分區函數

此函數傳入三個參數,列表nums,以及用來劃分要對列表進行快排操作的區域指針,first和last

  • 1、用一個“大”while循環來執行錯誤元素換位的情況,循環結束的條件為左指針大于右指針。

  • 2、里面再用“小”while循環來執行移動指針的情況,當指針指向元素相對中間數“正確”時,就繼續執行循環移動指針。
    需要用到兩個while,一個控制左指針,一個控制右指針。

  • 3、當上述兩個“小”while循環結束時,說明左右指針都指向了相對錯誤的元素,此時分為兩種情況:
    1、左指針小于右指針:說明此時分區工作還沒結束,對兩個元素進行換位(上面的圖1跟圖2),繼續執行元素換位的“大”while。
    2、左指針大于右指針:說明此時的分區工作已經結束了,對右指針指向的元素與中間數進行換位(上面的圖3),并且結束最外面的while循環。

  • 4、循環結束,可以返回中間數的index了,留著下一次遞歸調用函數的時候使用。

圖片理解參考 快速排序

代碼實現:


def quickSort(alist):
    quickSortHelper(alist, 0, len(alist) - 1)

def quickSortHelper(alist, first, last):
    if first < last:
        splitpoint = partition(alist, first, last)
        # 得到中點正確的位置,限制下面兩個遞歸的邊界
        quickSortHelper(alist, first, splitpoint - 1)
        quickSortHelper(alist, splitpoint + 1, last)

def partition(alist,first,last):
    pivotvalue = alist[first]
    leftmark = first+1
    rightmark = last
    done = False
    while not done:
        #如果左指針小于右指針,并且左指針指向的元素小于等于中間數,那么左指針就繼續走
        while leftmark <= rightmark and alist[leftmark] <= pivotvalue:
            leftmark = leftmark + 1
        #如果左指針小于等于右指針,并且右指針指向元素大于等于中間數,右指針就繼續走
        while rightmark >= leftmark and alist[rightmark] >= pivotvalue:
            rightmark = rightmark -1

        #左指針大于右指針時,done為True,結束循環
        if rightmark < leftmark:
            done = True
        #經過上面的指針操作以后,如果左指針還是小于右指針,則替換二者指向元素
        else:
            temp = alist[leftmark]
            alist[leftmark] = alist[rightmark]
            alist[rightmark] = temp

    #done為True,結束循環,替換中間元素與右指針指向元素的位置
    temp = alist[first]
    alist[first] = alist[rightmark]
    alist[rightmark] = temp
    ##返回此時中點的位置,用于下次遞歸分類劃分左右部分
    return rightmark

alist = [54,26,93,17,77,31,44,55,20]
quickSort(alist)
print(alist)
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,197評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,415評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 176,104評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,884評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,647評論 6 408
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,130評論 1 323
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,208評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,366評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,887評論 1 334
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,737評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,939評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,478評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,174評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,586評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,827評論 1 283
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,608評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,914評論 2 372

推薦閱讀更多精彩內容

  • 1、常用排序算法 2、快速排序法 基本思想:通過一趟排序將要排序的數據分割成獨立的兩部分,其中一部分的所有數據都比...
    Bling_ll閱讀 557評論 0 0
  • 動態規劃 111. 爬樓梯思路類似斐波那契數列注意考慮第 0 階的特殊情況 272. 爬樓梯 II思路類似上題,只...
    6默默Welsh閱讀 2,443評論 0 1
  • 最近越發聽到有會員問我:老師我腰粗用那個體式可以細點?老師我肚子大用那個體式可以平點?老師我腿粗用那個體式可以細點...
    渦孩緹閱讀 133評論 1 0
  • 這個月聽朋友說她在玩一款手游,挺好玩的,我抱著好奇心回宿舍叫上了兩個女生一起玩。也就是從那天下午開始到現在我們已沉...
    羅小扇閱讀 225評論 0 0
  • 學習濾鏡,首先要了解它是干什么的。濾鏡->就是給圖像 添加效果1、框架介紹(1)CoreImage(2)是一個圖像...
    闖先生的貓閱讀 6,952評論 4 12