常用算法(二)—高級算法


快速排序(quick sort)

首先任意選取一個數據(通常選用數組的第一個數)作為關鍵數據,然后將所有比它小的數都放到它前面,所有比它大的數都放到它后面,這個過程稱為一趟快速排序。

將數組分割成兩個數組之后再分別對剩下的兩個數組執行排序,這樣循環,直到剩一個元素。

import time,random
import copy

def cal_time(func):          #該裝飾器用來測量函數運行的時間
    def wrapper(*args,**kwargs):
        t1 = time.time()
        res = func(*args,**kwargs)
        t2 = time.time()
        print("%s running time: %s secs." % (func.__name__, t2 - t1))
        return res
    return wrapper

def query_sort(data,left,right):
    if left < right:
        mid = partition(data,left,right)
        query_sort(data,left,mid-1)
        query_sort(data,mid+1,right)

def partition(data,left,right):
    temp = data[left]
    while left < right:
        while left<right and data[right] >= temp:
            right -=1
        data[left] = data[right]
        while left<right and data[left] <= temp:
            left +=1
        data[right] = data[left]
    data[left] = temp
    return left

@cal_time
def query_sort_h(data):
    query_sort(data,0,len(data)-1)

@cal_time
def sort_fun(data):
    data.sort()
data = list(range(10000))
random.shuffle(data)
data1 = copy.deepcopy(data)
print(data)
query_sort_h(data)
sort_fun(data1)
print(data)

  

二叉樹

樹的特征和定義


  樹是一種重要的非線性 數據結構,直觀地看,它是 數據元素(在樹中稱為結點)按分支關系組織起來的結構,很象自然界中的樹那樣。 樹結構在客觀世界中廣泛存在,如人類社會的族譜和各種社會組織機構都可用樹形象表示。樹在計算機領域中也得到廣泛應用,如在編譯源程序時,可用樹表示源程序的語法結構。又如在 數據庫系統中,樹型結構也是信息的重要組織形式之一。一切具有層次關系的問題都可用樹來描述。
 

樹(Tree)是元素的集合。我們先以比較直觀的方式介紹樹。下面的數據結構是一個樹:

樹有多個節點(node),用以儲存元素。某些節點之間存在一定的關系,用連線表示,連線稱為邊(edge)。邊的上端節點稱為父節點,下端稱為子節點。樹像是一個不斷分叉的樹根。

每個節點可以有多個子節點(children),而該節點是相應子節點的父節點(parent)。比如說,3,5是6的子節點,6是3,5的父節點;1,8,7是3的子節點, 3是1,8,7的父節點。樹有一個沒有父節點的節點,稱為根節點(root),如圖中的6。沒有子節點的節點稱為葉節點(leaf),比如圖中的1,8,9,5節點。從圖中還可以看到,上面的樹總共有4個層次,6位於第一層,9位於第四層。樹中節點的最大層次被稱為深度。也就是說,該樹的深度(depth)為4。

 

如果我們從節點3開始向下看,而忽略其它部分。那么我們看到的是一個以節點3為根節點的樹:

三角形代表一棵樹

再進一步,如果我們定義孤立的一個節點也是一棵樹的話,原來的樹就可以表示為根節點和子樹(subtree)的關系:

 

上述觀察實際上給了我們一種嚴格的定義樹的方法:

1. 樹是元素的集合。

2. 該集合可以為空。這時樹中沒有元素,我們稱樹為空樹 (empty tree)。

3. 如果該集合不為空,那么該集合有一個根節點,以及0個或者多個子樹。根節點與它的子樹的根節點用一個邊(edge)相連。

上面的第三點是以遞歸的方式來定義樹,也就是在定義樹的過程中使用了樹自身(子樹)。由於樹的遞歸特征,許多樹相關的操作也可以方便的使用遞歸實現。我們將在后面看到。

樹的實現

樹的示意圖已經給出了樹的一種內存實現方式: 每個節點儲存元素和多個指向子節點的指針。然而,子節點數目是不確定的。一個父節點可能有大量的子節點,而另一個父節點可能只有一個子節點,而樹的增刪節點操作會讓子節點的數目發生進一步的變化。這種不確定性就可能帶來大量的內存相關操作,並且容易造成內存的浪費。

一種經典的實現方式如下:

 二叉樹: 

二叉樹是由n(n≥0)個結點組成的有限集合、每個結點最多有兩個子樹的有序樹。它或者是空集,或者是由一個根和稱為左、右子樹的兩個不相交的二叉樹組成。

特點:

(1)二叉樹是有序樹,即使只有一個子樹,也必須區分左、右子樹;

(2)二叉樹的每個結點的度不能大於2,只能取0、1、2三者之一;

(3)二叉樹中所有結點的形態有5種:空結點、無左右子樹的結點、只有左子樹的結點、只有右子樹的結點和具有左右子樹的結點。

 

二叉樹(binary)是一種特殊的樹。二叉樹的每個節點最多只能有2個子節點:

二叉樹

由於二叉樹的子節點數目確定,所以可以直接采用上圖方式在內存中實現。每個節點有一個左子節點(left children)和右子節點(right children)。左子節點是左子樹的根節點,右子節點是右子樹的根節點。

 

如果我們給二叉樹加一個額外的條件,就可以得到一種被稱作二叉搜索樹(binary search tree)的特殊二叉樹。二叉搜索樹要求:每個節點都不比它左子樹的任意元素小,而且不比它的右子樹的任意元素大。

(如果我們假設樹中沒有重復的元素,那么上述要求可以寫成:每個節點比它左子樹的任意節點大,而且比它右子樹的任意節點小)

二叉搜索樹,注意樹中元素的大小

二叉搜索樹可以方便的實現搜索算法。在搜索元素x的時候,我們可以將x和根節點比較:

1. 如果x等於根節點,那么找到x,停止搜索 (終止條件)

2. 如果x小於根節點,那么搜索左子樹

3. 如果x大於根節點,那么搜索右子樹

二叉搜索樹所需要進行的操作次數最多與樹的深度相等。n個節點的二叉搜索樹的深度最多為n,最少為log(n)。

 

二叉樹的遍歷

遍歷即將樹的所有結點訪問且僅訪問一次。按照根節點位置的不同分為前序遍歷,中序遍歷,后序遍歷。

前序遍歷:根節點->左子樹->右子樹

中序遍歷:左子樹->根節點->右子樹

后序遍歷:左子樹->右子樹->根節點

例如:求下面樹的三種遍歷

 

前序遍歷:abdefgc

中序遍歷:debgfac

后序遍歷:edgfbca

 

 二叉樹的類型

(1) 完全二叉樹——若設二叉樹的高度為h,除第 h 層外,其它各層 (1~h-1) 的結點數都達到最大個數,第h層有 葉子結點,並且葉子結點都是從左到右依次排布,這就是 完全二叉樹
(2) 滿二叉樹——除了葉結點外每一個結點都有左右子葉且葉子結點都處在最底層的二叉樹。
(3)平衡二叉樹——平衡二叉樹又被稱為AVL樹(區別於AVL算法),它是一棵二叉排序樹,且具有以下性質:它是一棵空樹或它的左右兩個子樹的高度差的絕對值不超過1,並且左右兩個子樹都是一棵平衡二叉樹

如何判斷一棵樹是完全二叉樹?按照定義,

教材上的說法:一個深度為k,節點個數為 2^k - 1 的二叉樹為滿二叉樹。這個概念很好理解,

就是一棵樹,深度為k,並且沒有空位。

首先對滿二叉樹按照廣度優先遍歷(從左到右)的順序進行編號。

一顆深度為k二叉樹,有n個節點,然后,也對這棵樹進行編號,如果所有的編號都和滿二叉樹對應,那么這棵樹是完全二叉樹。

 

image

堆排序

堆排序,顧名思義,就是基於堆。因此先來介紹一下堆的概念。
堆分為最大堆和最小堆,其實就是完全二叉樹。最大堆要求節點的元素都要大於其孩子,最小堆要求節點元素都小於其左右孩子,兩者對左右孩子的大小關系不做任何要求,其實很好理解。有了上面的定義,我們可以得知,處於最大堆的根節點的元素一定是這個堆中的最大值。其實我們的堆排序算法就是抓住了堆的這一特點,每次都取堆頂的元素,將其放在序列最后面,然后將剩余的元素重新調整為最大堆,依次類推,最終得到排序的序列。

 

堆排序就是把堆頂的最大數取出,

將剩余的堆繼續調整為最大堆,具體過程在第二塊有介紹,以遞歸實現

剩余部分調整為最大堆后,再次將堆頂的最大數取出,再將剩余部分調整為最大堆,這個過程持續到剩余數只有一個時結束

import time,random,copy
from cal_time import cal_time,nor_sort

def sift(data,low,high):   #來一次調整
    i= low         #low為根節點,父節點
    j= 2 * i + 1    #i的左孩子
    tmp = data[i]
    while j <= high: #沒有超過數組范圍時
        if j < high and data[j] < data[j + 1 ]:#如果j小於high也就是說左孩子小於最邊界的high那么這個節點肯定有右孩子,
            j +=1   #這個時候對比左孩子和右孩子的大小,如果右孩子大於左孩子,那么這個時候把J置為右孩子,J的左右就是表示兩個孩子里面最大的那個
        if tmp < data[j]: #如果父親的值小於孩子的值,這個時候開始換位置
            data[i] = data[j]    #j的值換為父親,i的值換為孩子
            i =j             #把I的位置調整到之前比父親大的那個孩子的位置,
            j = 2* i + 1     #開始比較當前這個孩子和他自己孩子的大小,找到它的左孩子,定義為j
        else:     #如果兩個孩子都沒父親大
            break    #什么也不干,跳出當前循環
    data[i] = tmp    #把最初始i的值賦給調整之后的位置

@cal_time
def heap_sort(data):
    n = len(data)
    for i in range(n //2 -1,-1,-1):#取最后一個有孩子的父節點,倒序遍歷
        sift(data,i,n-1)  #調整
    for i in range(n-1,-1,-1):
        data[0],data[i] = data[i],data[0]
        sift(data,0,i-1)

data = list(range(10000))
random.shuffle(data)
data1 = copy.deepcopy(data)
print(data)
heap_sort(data)
print(data)
data1 = nor_sort(data1)
print(data1)

 

歸並排序

假設現在的列表分兩段有序,如何將其合成為一個有序列表

將序列垂直分割成兩個有序序列,然后取每個序列的第一項對比,小的取出來然后繼續對比,當某一序列取完之后就把另一個序列剩下的全部放到新的序列里面

將數組細分,先分解到數個有序的序列,比如只包含一個元素的序列是有序的,然后再開始合並,所以稱該算法為歸並排序。

代碼示例:

import time,random,copy
from cal_time import cal_time,nor_sort

def merge(data,low,mid,high):
    i = low
    j = mid + 1
    ltmp = []
    while i <= mid and j <= high:
        if data[i]<data[j]:
            ltmp.append(data[i])
            i += 1
        else:
            ltmp.append(data[j])
            j += 1
    while i <= mid:
        ltmp.append(data[i])
        i += 1
    while j <= high:
        ltmp.append(data[j])
        j +=1
    data[low:high+1] = ltmp

def _merge_sort(data,low,high):
    if low<high:
        mid = (low+high)//2
        _merge_sort(data,low,mid)
        _merge_sort(data,mid+1,high)
        merge(data,low,mid,high)

@cal_time
def merge_sort(data):
    _merge_sort(data,0,len(data)-1)

data = list(range(10000))
random.shuffle(data)
data1 = copy.deepcopy(data)
print(data)
merge_sort(data)
nor_sort(data1)
print(data)

  

 快速排序、堆排序、歸並排序的區別

三種排序算法的時間復雜度都是O(nlogn) 一般情況下,就運行時間而言: 快速排序 < 歸並排序 < 堆排序

三種排序算法的缺點: 快速排序:極端情況下排序效率低 歸並排序:需要額外的內存開銷 堆排序:在快的排序算法中相對較慢

希爾排序(shell sort)

希爾排序(Shell Sort)是插入排序的一種。也稱縮小增量排序,是直接插入排序算法的一種更高效的改進版本,該方法的基本思想是:先將整個待排元素序列分割成若干個子序列(由相隔某個“增量”的元素組成的)分別進行直接插入排序,然后依次縮減增量再進行排序,待整個序列中的元素基本有序(增量足夠小)時,再對全體元素進行一次直接插入排序。因為直接插入排序在元素基本有序的情況下(接近最好情況),效率是很高的,因此希爾排序在時間效率比直接插入排序有較大提高,希爾排序每趟並不使某些元素有序,而是使整體數據越來越接近有序;最后一趟排序使得所有數據有序。

首先要明確一下增量的取法:

      第一次增量的取法為: d=count/2;

      第二次增量的取法為:  d=(count/2)/2;

      最后一直到: d=1;

看上圖觀測的現象為:

        d=3時:將40跟50比,因50大,不交換。

                   將20跟30比,因30大,不交換。

                   將80跟60比,因60小,交換。

        d=2時:將40跟60比,不交換,拿60跟30比交換,此時交換后的30又比前面的40小,又要將40和30交換,如上圖。

                   將20跟50比,不交換,繼續將50跟80比,不交換。

        d=1時:這時就是前面講的插入排序了,不過此時的序列已經差不多有序了,所以給插入排序帶來了很大的性能提高。

代碼示例:

import random,copy
from cal_time import cal_time,nor_sort

def shell_sort(data):
    gap = len(data) // 2
    print(gap)
    while gap >=1:
        for i in range(gap,len(data)):
            tmp = data[i]
            j = i-gap
            while j >= 0 and tmp < data[j]:
                data[j + gap] = data[j]
                j -=gap
            data[i-gap] = tmp
        gap //=2

data = list(range(10000))
random.shuffle(data)
data1 = copy.deepcopy(data)
print(data)
shell_sort(data)
print(data)
data1 = nor_sort(data1)
print(data1)

  

各排序方法總結:

 備注:1、深度遞歸調用有時會出錯需要設置遞歸保護代碼:

sys.setrecursionlimit(100000)

 2、切記不能給遞歸函數加裝飾器,這樣的話每次遞歸裝飾器都會執行一次


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM