數據結構與算法之查找和排序

必備知識點

時間複雜度

時間複雜度是用來估算算法運行速度的一種方式,一般採用大O表示法。
須要注意如下幾點:python

  • 時間複雜度指的不是算法運行的時間,而是算法運行的增速。
  • 時間複雜度是估算,一些非必要的會省略。
  • 一般表示爲O(n),其中n爲操做數。
    快速判斷時間複雜度的方法:
  • 若是發現循環數減半,那麼複雜度就是logn。
  • 有幾層循環就是n的幾回方,不要在乎具體循環幾回。

遞歸

遞歸比較容易理解,有如下兩個特徵:git

  • 調用自身
  • 有 終止條件
    咱們能夠經過sys模塊來進行查看默認最大執行次數,同時 sys.setrecursionlimit() 也能進行更改,默認是1000次.
#遞歸實現斐波那契數列
def fibnacci(n):
    if n=0 or  n=1:
        return 1
    else:
        return fibnacci(n-1)+fibnacci(n-2)  #這就是遞歸的精髓,把複雜重複的運算抽絲剝繭,每遞歸一次就簡化一次

#斐波那契數列能夠用更簡單的方法實現
def fibnacci(n):
    a=b=c=1
    for i in range(2,n+1):
        c=a+b
        a=b
        b=c
    return c

#遞歸實現漢諾塔
def hanoi(n, A, B, C):
    if n > 0:
        hanoi(n-1, A, C, B)
        print('%s->%s' % (A, C))
        hanoi(n-1, B, A, C)

查找

簡單查找

簡單查找就是按順序查找,直到查到指定元素,時間複雜度爲O(n)。算法

二分查找

二分查找是對簡單查找的一種優化,可是操做的只能是有序數組,就是經過中間值和需求數比較,經過比較結果來改變左右範圍。
須要注意的是,不要經過切片改變列表,那樣會加大空間複雜度。
尾遞歸的定義:尾遞歸就是全部遞歸語句都是在函數的最後出現的,正常是至關於循環的複雜度,可是python內沒有優化。shell

def bin_search(li, val):
    low = 0
    high = len(li)-1
    while low <= high: # 只要候選區不空,繼續循環
        mid = (low + high) // 2
        if li[mid] == val:
            return mid
        elif li[mid] < val:
            low = mid + 1
        else: # li[mid] > val
            high = mid - 1
    return -1
#遞歸實現二分法
lst = [1, 2, 33, 44, 555]
def func(left,right,n):
    if left <= right:
        mid = (right + left) // 2
        if n > lst[mid]:
            left = mid+1
        if n < lst[mid]:
            right = mid - 1
        if n == lst[mid]:
            return( "找到了")
    else:
        return("沒找到")
    return func(left,right,n)
ret = func(0,len(lst)-1,44)
print(ret)
#區別:本方法利用切片,改變原序列表,增長了空間複雜度
lst = [1, 2, 33, 44, 555]
count = 0
def func(lis,n):
    left = 0
    right = len(lis)-1
    global count
    count = count + 1
    print(count)
    if left <= right:
        mid = (right + left) // 2
        if n > lst[mid]:
            return func(lis[mid+1:],n)
        elif n < lst[mid]:
            return func(lis[:mid], n)
        else:
            return( "找到了")
    else:
        return("沒找到")
ret = func(lst,44)

一個小技巧

主要思想爲:新建列表做爲索引,若是一個數的索引存在,說明這個數也存在.api

lis = [2,4,6,7]                        #就是計數排序的反向使用,計數排序會在線性排序模塊裏說
n = 3            #查找n
lst = [0,0,0,0,0,0,0,0]   #建立一個元素均爲0的列表,元素個數爲lis中最大的數字加1
li = [0,0,1,0,1,0,1,1]  #把 lis 中對應的數字值變爲1
if li[3] == 1:
    print("存在")
else:
    print("不存在")

排序

排序算法是有穩定和不穩定之分。
穩定的排序就是保證關鍵字相同的元素,排序以後相對位置不變,所謂關鍵字就是排序的憑藉,對於數字來講就是大小。
排序算法的關鍵點是分爲有序區和無序區兩個區域。數組

冒泡排序

冒泡排序思路:數據結構

  • 比較列表中相鄰的兩個元素,若是前面的比後邊的大,那麼交換這兩個數。
  • 這就會致使每一次的冒泡排序都會使有序區增長一個數,無序區減小一個數。
  • 能夠認爲獲得一個排序完畢的列表須要n次或者n-1次。n-1次是由於最後一次不須要進行冒泡了,固然n或n-1的獲得的列表是同樣的。
    冒泡排序是穩定的。
#基礎冒泡
def bubble_sort(li):
    for i in range(len(li)-1):  #第一層循環表明處於第幾回冒泡
        for j in range(len(li)-i-1):    #第二層循環表明無序區的範圍
            if li[j]>li[j+1]:
                li[j],li[j+1]=li[k+1],li[j]

若是考慮冒泡的最好狀況,也就是冒泡沒有進行到n次的時候就已經不出現j>j+1了,那麼排序已經進行完畢。app

def bubble_sort(li):
    for i in range(len(li)-1):  #第一層循環表明處於第幾回冒泡
        a= 1
        for j in range(len(li)-i-1):    #第二層循環表明無序區的範圍
            if li[j]>li[j+1]:
                li[j],li[j+1]=li[k+1],li[j]
                a=2
        if a=1:
            break

選擇排序

選擇排序的思路:函數

  • 一次遍歷找出最小值,放到第一個位置。
  • 再一次遍歷找最小值,在放到無序區第一個位置。
  • 與冒泡同樣是進行n或n-1次
  • 每次都會讓有序區增長一個元素,無序區減小一個元素。那麼進行第i次的時候,它的第一位置的索引就是i。注意是無序區。
    選擇排序是不穩定的,跨索引交換(對比於相鄰)就是不穩定的。
def select_sort(li):
    for i in range(len(li)-1):
        min_pos=i   #第幾回,無序區的第一個位置的索引就爲幾減一
        for j in range(i+1,len(li)):
            if li[j]<li[min_pos]:   #min_pos會隨着循環變換值
                min_pos=j
        li[i],li[min_pos]=li[min_pos],li[i]

插入排序

插入排序的思路:優化

  • 在最開始有序區就把列表的第一個元素就放入有序區(這種有序是相對有序)。
  • 在無序區第一個位置取出一個元素與有序區原本存在的元素進行比較,根據大小插入。
  • 插入排序須要n-1次得出結果。
  • 每次進行插入比較就是一步步的往前進行比較,也就是位置因此要一次次的減1,可能出現位置在最前面也就是插入位置索引爲0,也多是在中間,因此有兩種狀況。
def insert_sort(li):
    for i in range(1,len(li)):  #i表示須要進行插入的元素的位置
        j=i-1   #j的初始位置,也就是無序區第一個元素的位置
        while j!=-1 and li[j]>li[i]:#只要可以與無序區元素進行比較,循環就不中止          
        #跳出循環的狀況只有是有序區進行比較的元素沒了,可是跳出循環時與li[j]<=li[i]時執行的語句是同樣的,都是li[j+1]=li[i],因此進行一個合併,減小代碼量
            li[j+1]=li[j]   #進行比較的有序區索引加一
            j-=1    #進行比較元素的索引減一
        li[j+1]=li[i]   #也就是成爲0號元素

快速排序

快排思路:

  • 取第一個元素,使元素歸位。
  • 歸位的意義爲列表分爲兩部分,左邊都比該元素小,右邊都比該元素大。
  • 遞歸,進行屢次歸位。
    快速排序的時間複雜度爲 nlogn。
def _quick_sort(li,left,right):
    if left<right:  #待排序區域至少有兩個元素,left和right指的是索引
        mid = partition(li,left,right)
        _quick_sort(li,left,mid-1)
        _quick_sort(li,mid+1,right)
        
def quick_sort(li): #包裝一下,由於循環不能直接遞歸,會很是慢
    _quick_sort(li,0,len[li]-1)
    
def partition(li,left,right):        #此函數的意義是歸位
    tmp=li[left]        #left爲第一個元素的索引,也就是須要進行歸位的元素的索引
    while left<right:
        #注意,小的在左邊,大的在右邊
        while li[right]>tmp and left<right: #當right的值小於tmp是退出
            right-=1    #進行下一個right
        li[left]=li[right]    #把left位置放比tmp小的right
        while li[left]<=tmp and left<right:
            left+=1
        li[right]=li[left]  #把right位置放比tmp大的left
    li[left]=tmp        #把tmp放在left=right時剩下的位置
    return left

在Haskell中只須要6行

quicksort :: (Ord a) =>[a] ->[a]
quicksort [] = []
quicksort (x:xs) = 
    let smallersort = quicksort[a|a<- xs, a <=x]    -- 屬於xs,而且小於x
    let biggersort = quick[a|a<- xs,a>x]
    in smallersort ++ x ++ biggersort

堆排序

知識儲備:

  • 樹是一種數據結構,能夠經過遞歸定義。
  • 樹是由n個節點構成的集合
    • 若是n=0,那麼是一顆空樹。
    • 若是n>0,那麼存在 一個根節點,其餘的節點均可以單拎出來做爲一個樹。
  • 根節點,就是回溯後存在的節點。
  • 葉子節點,就是沒有下層節點的節點。
  • 樹的深度能夠粗略認爲就是節點最多的分支有幾個節點
  • 度,度就是一個節點由幾個分支,也就是說有幾個子節點
  • 父節點和子節點索引的關係,若父節點的索引爲i,左子節點索引爲2i+1,右子節點的索引爲2i+2,子節點找父節點,i=(n-1)//2
  • 二叉樹:度最多有兩個的樹。
  • 滿二叉樹:一個二叉樹,每一層的節點數都是最大值,也就是2,那麼它就是滿二叉樹。
  • 徹底二叉樹:葉子節點只能出如今最下層和次下層,而且最下面一層的節點都集中在該層最左側,滿二叉樹是一種特殊的徹底二叉樹。

二叉樹的存儲方式:

  • 鏈式存儲,在以後的數據結構博客介紹。
  • 順序存儲,順序存儲就是列表。結構就是從上到下從左到右。
  • 堆:堆是一顆徹底二叉樹,分爲大根堆和小根堆:
    • 大根堆就是任何的子節點都比父節點小。
    • 小根堆就是任何一個子節點都比父節點大。
  • 堆的向下調整性質:當根節點的左右子樹都是堆,那麼就能夠將其經過向下調整來創建一個堆
  • 向下調整就是把根節點單獨拿出來,讓它子節點盡行大小比較,而後把根節點插入到子節點位置,子節點成爲新的根節點,如此遞歸,直到知足堆的定義。

堆排序也就是優先隊列,進行屢次向下調整,得出一個根堆,而後根據索引從後往前挨個輸出節點。
向下調整

def sift(li,low,high):
    i=low   #相對根節點
    j=2*i+1     #它的左子節點位置
    tmp=li[i]   #根節點元素大小
    while j<=high:
        if j<high and li[j]<li[j+1]:    #先判斷左右節點大小,j<high是由於可能出現沒有有節點的狀況
            j+=1
        if tmp <li[j]:      #再判斷左節點或有節點與根節點的大小
            li[i]=li[j]     #把左右節點移動到根節點
            i=j             #把相對根節點移動到下一層
            j=2*i+1         #新的子節點索引
        else:
            break
    li[i]=tmp               #最後把原來的根節點放到索引i上

從堆中取出節點元素

def heap_sort(li):
    for low in range(len(li)//2-1,-1,-1):   #構造堆,low的取值是倒序,從後面到0
        sift(li,low,len(li)-1)      #high被假設是固定的,由於它爲最小對結果不會影響。
    for high in range(len(li)-1,-1,-1): #取出時high一直是動態的,讓取出的low不參加以後的調整,也就是構建新堆的過程
        li[0],li[high]=li[high],li[0]   #把得出的無序區最後一個值,放到根結點處進行構建新堆
        sift(li,0,high-1)   #

python的heapq內置模塊

  • nlargest,nsmallest 前幾個最大或最小的數
  • heapify(li) 構造堆
  • heappush 向堆裏提交而後構造堆
  • heappop 彈出最後一個元素

歸併排序

歸併排序思路:

  • 一次歸併:含義就是給定一個列表,分爲兩段有序,讓它成爲一個總體有序的列表。
  • 一次歸併的方法:把兩段有序列表,兩兩比較,把較小的那個元素拿出來,若一方元素數量爲0,那麼就將另外一方全部元素取出。
  • 歸併排序: 先分解後合併,分解爲單個元素,那麼單個元素就是有序的,而後再兩兩一次歸併,獲得有序列表。
  • 也就是把歸併排序當作屢次的一次歸併。
def merge(li, low, mid, high):  #mid爲兩段有序分界線左邊第一個數的索引
    # 列表兩段有序: [low, mid] [mid+1, high]
    i = low     #i指向左半邊列表進行比較的元素
    j = mid + 1 #j指向右半邊列表進行比較的元素
    li_tmp = [] #比較出較小的元素暫存的位置
    while i <= mid and j <= high:   #當左右兩側比較的元素都不爲空時
        if li[i] <= li[j]:  #左邊小,左邊元素拿到暫存li_tmp中,左邊指針向右移動
            li_tmp.append(li[i])
            i += 1
        else:               #右邊小,右邊元素拿到li_tmp中,右邊元素的指針向左移動
            li_tmp.append(li[j])
            j += 1
    while i <= mid:     #將剩餘的元素都拿到li_tmp中
        li_tmp.append(li[i])
        i += 1
    while j <= high:
        li_tmp.append(li[j])
        j += 1
    for i in range(low, high+1):    #把li_tmp中的元素放到li中
        li[i] = li_tmp[i-low]
    # 也能夠這樣移動: li[low:high+1] = li_tmp

def _merge_sort(li, low, high): #排序li從low到high的範圍
    if low < high:
        mid = (low + high) // 2 #開始遞歸分散
        _merge_sort(li, low, mid)
        _merge_sort(li, mid+1, high)
        merge(li, low, mid, high)   #合併

希爾排序

希爾排序思路:

  • 希爾排序是一種分組插入排序算法,能夠理解爲是對插入排序的優化。
  • 首先去一個整數d1=n/2,將元素分爲d1個組,每組相鄰兩個元素之間的距離爲d1,在各組內直接插入排序。
  • 接下來,去第二個元素d2=d1/2,重複上一步,直到di=1。
  • 即全部元素在同一組內進行直接插入排序。
  • 希爾排序每次都會讓列表更加接近有序,在那過程當中不會使某些元素變得有序。
def shell_sort(li):
    gap = len(li) // 2
    while gap > 0:    #接下來進行的其實就是插入排序的代碼,只不過無序區起始是從gap也就是從gap開始。
        for i in range(gap, len(li)):
            tmp = li[i]
            j = i - gap
            while j >= 0 and tmp < li[j]:
                li[j + gap] = li[j]
                j -= gap
            li[j + gap] = tmp
        gap /= 2

線性時間排序

線性時間排序都有侷限性,並不經常使用。

計數排序

計數排序思路:

  • 計數排序時間複雜度爲O(n),是一種特殊的桶排序。
  • 首先必須知道列表中數的範圍
  • 加入最大值的大小爲d,那麼就創建一個元素數目爲d的值均爲0的列表,這也是計數排序的侷限性,若是最大數很大的化,計數排序會很佔用內存。
  • 遍歷原列表,若是值爲t的某個數出現了一次,那麼就在新建列表中索引爲t的元素的值上加1.
  • 最後再遍歷新建列表,把索引做爲值,值做爲出現次數依次列出。
def count_sort(li,max_num):
    count = [0 for _ in range(max_num+1)]        #列表表達式,以前的博客有寫,和Haskell中同樣_表明咱們徹底再也不在意它的值
    for i in li:
        count[i]+=1        #出現一次,值加一
    li = []
    for i,v in enumerate(count):        #i是元素,v是出現的次數
        for _ in range(v):
            li.append(i)

桶排序

桶排序思路:

  • 桶排序是對計數排序的一種改造,時間複雜度O(nk)
  • 計數排序能夠當作每一個桶裏只裝一個元素的桶排序。
  • 首先根據待排序列表的數據分佈進行分桶,桶是有順序且不間斷的。
  • 而後分別對桶裏的元素進行排序
  • 桶內元素排序完成以後,總體列表也就排序成功了
  • 桶內排序能夠採用任何算法
  • 內嵌排序會因爲桶排序而下降時間複雜度,也就是以空間換時間,乘法變加法。
  • 得根據實際狀況寫代碼,例子就不寫了,思路很簡單。

基數排序

要說基數排序,先說多關鍵字排序。
多關鍵字排序思路:

  • 多關鍵字排序就是,在XX條件的基礎上進行排序。
  • 好比平常場景中的,對薪資相等的員工,由年齡從大到小進行排序。
  • 排序的方式爲 先有年齡從大到小進行排序,再對薪資進行排序。
  • 其中年齡被稱爲低關鍵字,薪資爲高關鍵字,也就是說,先行條件爲高,先行條件的基礎上的條件爲低。
  • 緣由是穩定排序,大小相等的元素,其相對位置不變。
    有了這個思路,再說基數排序:
  • 基數排序是針對整數的。
  • 能夠思考兩位數的大小排序,個位數爲低關鍵字,十位數就是高關鍵字。
  • 基數排序就是把個位數0-9分紅10個桶,依次進行排序。
  • 由於桶的索引是從0-9,那麼只要放進桶裏就至關於已經完成了一次排序

預備知識:

  • 如何獲取一個整數的個位數、十位數,百位數。。。
    • 首先,得出列表中的最大值,除以10取餘能夠得到個位數
    • 其次,除以10取整,會捨去個位數,變成以原十位做爲個位的數
    • 思路就是不斷的取整,取餘
    def get_dight(num,i):
      #i爲1時,取個位數,i爲2時取十位數
      return num//(10 * i)%10

    還能夠轉成列表後根據索引得出,思路和上面是同樣的,就當科普了。

    #int轉list
    def int2list(num):
      li=[]
      while num>0:
          li.append(num%10)
          num //=10
      li.reverse_list()    #再科普一個列表反轉吧
      return li
    列表反轉思路:
    • 把列表分爲左右兩邊
    • 對應的元素交換位置
    def reverse_list(li):
      n=len(li)
      for i in range(n//2):
          li[i],li[n-i-1] = li[n-i-1],li[i]
      return li
  • 說完列表反轉再說一個int反轉吧
    • 整數有正負之分
    • 首先求出num的餘數a,num地板除獲得b。
    • a*10+b,獲得後兩位數的反轉
    • 重複上述兩步,直到地板除爲0。
    def reverse_int(num):
      is_neg = False
      if num < 0:
          is_neg = True
          num = -1 * num
      res = 0
      while num >0:
          res = res * 10
          res = res + num%10
          num = num // 10
      if is_neg:
          res = res * -1
      return res

    。。
    直接上基數排序:

    def max_nu(li):
      for i in range(len(li)-1):
          max_pos=i
          for j in range(i+1,len(li)-1):
              if li[i] < li[j]:
                  max_pos=j
    def radix_sort(li):
      max_num=max_nu(li)
      i = 0                                      #從個位數開始,
      while (10 ** i <= max_num):    #這裏注意是能夠 = 的,要考慮 10 100 的狀況
          buckets = [[] for i in range(0,10)]
          for val in li:
              digit = li // (10 ** i)%10
              buckets[digit].append[val]    #以餘數做爲索引,放入桶內,第一次循環是個位數,第二次循環是十位數
          li = []
          for bucket in buckets:
              for val in bucket:        #在桶中依次取出數據
                  li.append(val)        #進行一次排序後放回li中
          i+=1
相關文章
相關標籤/搜索