數據結構與算法之線性結構和樹結構

什麼是數據結構

數據結構是指相互之間存在着一種或多種關係的數據元素的集合和該集合中數據元素之間的關係的組成。python

  • 數據結構就是設計數據以何種方式存儲在計算機中,列表、字典等都算是數據結構。linux

  • 程序=數據結構+算法,數據結構屬於靜態的部分,算法的調用爲動態部分算法

數據結構的分類

根據邏輯結構劃分:數據庫

  • 線性結構:數據結構中的元素一對一的關係,一前驅,一後繼。
  • 樹結構:數據結構中元素一對多的關係,一前驅,多後繼。
  • 圖結構:數據結構中元素存在多對多的關係,多前驅,多後繼,我也不會。
    • 判斷一個圖形能不能一筆畫完,就判斷它的奇數度節點數目是否爲0或2.這種能一筆畫完的就是歐拉圖,奇數度節點爲四個,就是兩筆畫完。

線性結構

列表

列表和數組

python中的列表和其餘語言中的數組很類似,區別爲:編程

  • 數組是定長的。
  • 數組的數據類型也必須一致。
  • 對列表或數組來講,它們的下標操做是最快的。

列表解決的變長問題的方式

  • 假設一開始在內存中分配了四個元素存儲的空間,那麼前四個元素的append操做不會出現問題。
  • 當第五次append操做時,會先在內存中分配一個可以存儲八個元素的空間,也就是翻倍。
  • 而後進行復制,把之前的四個元素依次放到相應的位置上。
  • 若再次超出長度,則繼續執行上述操做。
  • 也就是使用了動態表的原理

append操做會不會使速度變慢?數組

  • 根據攤還分析,沒有變長時的append和變長時的append均攤,最後的複雜度時O(3).
  • append越日後,變長時的出現頻率就會越小
  • 浪費了一部分空間,最壞狀況應該是浪費了長度除二減一的空間。

列表解決多數據類型問題的方式

  • 對於純整數的數組,它的每個元素佔4個字節,那麼就事先計算好內存分配的大小,計算方法爲:- 第一個元素的地址+元素個數 乘 4
  • python的列表裏存的不是值,而是指向這個值的內存地址。
  • 地址的大小是同樣的,32位裏地址是4個字節,64位裏地址是8個字節。
  • 這種方法的缺點是內存開銷翻倍,這也是python被人詬病的地方。

相關知識點

老是能聽到一個詞 堆棧 ,堆(heap)和棧(stack)是兩個東西,傳統的編程語言中把內存分爲兩個地方,堆空間和棧空間,堆存儲的是一些動態生成的對象,與數據結構中的堆是不一樣的,棧空間由系統調用,存放函數的參數值,局部變量的值。
應該是早年間翻譯的問題,通常聽到堆棧指的就是棧。安全

  • 棧是一個數據集合,能夠理解爲只能在一端進行插入和刪除操做的列表。
  • 棧的特色:後進先出(last-in,first-out)
    • 棧頂:操做永遠在棧頂。
    • 棧底:最後一個元素。
  • 棧的基本操做:
    • 進棧(壓棧):push
    • 出棧:pop
    • 取棧頂: gettop
  • 關於出棧順序的問題:
    • 對於某個元素,若是進展順序在它前面的元素出棧時在它後面,那麼前面的元素順序是相反的。
    • 不知道說的明不明白
    • 卡特蘭數,n個數的出棧順序,就是卡特蘭數的第n項。
#棧的python實現
class Stack:
    def __init__(self,size):
        self.size=size
        self.top = 0
        self.lst=[]
    
    def push(self,a):
        if self.top = self.size:
            raise StackFullError("stackoverflow") 
        self.lst.insert(self.top,a)
        self.top+=1
    
    def pop(self):
        if self.top = 0:
            raise StackEmptyError()
        b = self.list[self.top]
        self.lst.pop(self.top)
        returm b

棧的應用--括號匹配問題

  • 給定一個字符串,問其中字符串是否匹配。
  • 括號自己知足棧的性質
  • 匹配失敗的狀況:
    • 括號不匹配
    • 匹配完畢棧沒空
    • 棧空了又進元素
    def brace_match(s):
      stack = []
      d ={'(':')','[':']','{':'}'}
      for ch in s:
          if ch in {'(','[','{'}:
              stack.append(ch)
          elif len(stack)==0:
              print('多了%s' %ch)
              return False
          elif d[stack[-1]] == ch:
              stack.pop()
          else:
              print('%s不匹配'%ch)
      if len(stack)==0:
          return True
      else:
          print("未匹配")
          return False

隊列

相關知識點:

隊列是一個數據集合,僅容許在列表的一端插入,另外一端刪除。數據結構

  • 進行插入的時隊尾,進行刪除操做的是隊首,插入和刪除操做也被稱爲進隊(push)和出隊(pop)。
  • 隊列的性質:先進先出(first-in,first-out)
  • 雙向隊列:兩邊都能進行插入刪除操做的隊列。

隊列的數組實現:

  • 簡單的pop(0)操做複雜度太高,不採用。
  • 因爲數組定長,不能繼續添加數據,若是是列表,出隊的操做就會出現空位,因此想辦法讓數組變成一個圓環。app

  • 設置兩個指針,隊首指針front,隊尾指針rear。
  • 因爲,隊列滿的時候和隊列空的時候rear和front都在一個位置,那麼就沒法判斷了。因而設置成隊列滿的時候減去一作爲隊滿的標誌。
  • 這種隊列就叫作環形隊列。
    • 當隊尾指針front=最大長度+1時,再前進一個位置就自動到0.
    • 實現方式:求餘數運算
      • 隊首指針前進1:front=(front+1)%maxsize
      • 隊尾指針前進1:rear=(rear+1)%maxsize
      • 隊空條件:rear=front
      • 隊滿條件:(rear+1)%maxsize=front
class queue:
    def __init__(self, capacity = 10):
        self.capacity = capacity
        self.size = 0
        self.front = 0
        self.rear = 0
        self.array = [0]*capacity
 
    def is_empty(self):
        return 0 == self.size
 
    def is_full(self):
        return self.size == self.capacity
 
    def enqueue(self, element):
        if self.is_full():
            raise Exception('queue is full')
 
        self.array[self.rear] = element
        self.size += 1
        self.rear = (self.rear + 1) % self.capacity
 
    def dequeue(self):
        if self.is_empty():
            raise Exception('queue is empty')
 
        self.size -= 1
        self.front = (self.front + 1) % self.capacity
 
    def get_front(self):
        return self.array[self.front]

經過兩個棧作一個隊列的方法

  • 1號棧進棧 模擬進隊操做。
  • 2號站出棧,若是2號棧空,把1號站依次出棧並進2號棧,模擬出隊操做。
  • 經過攤還分析,時間複雜度仍是O(1)。
class queue:
    def __init__(self,size):
        self.a = []
        self.b = []
        self.size = size

    def popleft(self):
        if not self.b and self.b is None:
            el = self.b.pop(-1)
            self.append(el)
            self.a.pop(-1)
        else:
            raise Exception("empty")

    def append(self,item):
        if self.b<self.size:
            self.b.append[item]
        else:
            raise Exception("FUll")

python關於隊列的模塊

import queue    #涉及線程安全用queue
from collections import deque   #經常使用解題的用deque

q = deque()     #是一種雙向隊列,popleft出隊

#模擬linux命令 head和tail,假如是tail 5
deque(open('a.text','r',encooding='utf8'),5)
#創建一個定長的隊列,當隊列滿了以後,就會刪除第一行,繼續添加

鏈表

相關知識點:

鏈表就是非順序表,與隊列和棧對應。編程語言

  • 鏈表中每個元素都是一個對象,每一個對象稱爲一個節點,包含有數據域key和指向下一個節點的next,經過各個節點之間的相互鏈接,最終串聯成一個鏈表。

  • 在機械硬盤中,文件就是以鏈表的形式存儲的。
  • 以FAT32爲例,文件的單位是文件塊(block),一個文件塊的大小是4k,一個文件的內容是由鏈表的方式鏈接文件塊組成的。
  • 鏈表的第一個節點被稱爲頭節點,數據能夠是空的,也能夠有值。
  • 頭節點爲空也是爲了表示空鏈表,也叫作帶空節點的鏈表,頭節點也能夠記錄鏈表的長度

節點定義

class Node(object):
    def __init__(self,item):
        self.data=data
        self.next=None
#eg
a=Node(1)
b=Node(2)
c=Node(3)
a.next=b
b.next=c    #鏈表的最後一個節點的next就爲None

鏈表類的實現

class LinkList:
    def __init___(self,li,method='tail'):
        self.head = None
        self.tail = None
        if method == 'head':
            self.create_linklist_head(li)
        if method == 'tail'
            self.create_linklist_tail(li)
        else:
            rais ValueError('unsupport')
            
    #頭插法
    def create_linklist_head(self,li):
        self.head = Node(0)
        for v in li:
            n = Node(v)
            n.next = self.head.next     #當插入下一個元素時,應該與下一個節點鏈接後再跟頭節點鏈接
            self.head.next = n
            self.head.data += 1
    
    #尾插法
    def create_linlist_tail(self,li):        #不斷更新尾巴
        self.head = Node(0)
        self.tail = self.head
        for v in li:
            p = Node(v)
            self.tail.next = p
            self.tail = p
            self.head.data += 1
            
    #鏈表的遍歷輸出
    def traverse_linlist(self):
        p = self.head.next
        while p:
            yield p.data
            p = p.next

插入刪除總結

  • 插入
#p表示待插入節點,curNode表示當前節點
p.next = curNode.next   #不能當前鏈接直接斷開
curNode,next = p
  • 刪除
p = curNode.next
curNode.next = p.next
del p   #不寫也同樣,引用計數,python的內存回收機制

雙鏈表

雙鏈表中每一個節點有兩個指針:一個指向後面節點、一個指向前面節點。
節點定義:

class Node(object):
    def __init__(self, item=None):
        self.item = item
        self.next = None
        self.prior = None

雙鏈表的插入和刪除

  • 插入
p.next = curNode.next
curNode.next.prior = p
p.prior = curNode
curNode.next = p
  • 刪除
p = curNode.next
curNode.next = p.next
p.next.prior = curNode
del p

鏈表的複雜度分析

鏈表與列表相比

  • 按元素值查找:列表可使用二分法是O(logn),鏈表是O(n)
  • 按下標查找:O(1),O(n)
  • 再某元素後插入:O(n),O(1)
  • 刪除莫元素:O(n),O(1)
    總的來講鏈表再插入和刪除某元素的操做時明顯快於順序表,並且經過雙鏈表能夠更容易實現棧和隊列。

哈希表

直接尋址表

哈希表就是直接尋址表的改進。當關鍵字的全域U比較小時,直接尋址是一種簡單有效的方法。

  • 全域的意思就是它的取值範圍。
  • 也就是直接把關鍵字爲key的value放在key的位置上
    直接尋址的缺點:
  • 當域U很大時,須要消耗大量內存。
  • 若是U很大,但關鍵字不多,浪費大量空間。
  • 若關鍵字不是數字則沒法處理。
    直接尋址表的改進:
  • 構建大小爲m的尋址表T
  • key爲k的元素放到h(k)上
  • h(k)是一個函數,其將域U映射到表T(0,1,..,m-1)

哈希表

哈希表是一個經過哈希函數計算數據存儲位置的線性表的存儲結構,又叫作散列表。

  • 哈希表由一個直接尋址表和一個哈希函數組成。
  • 哈希函數h(k)將元素關鍵字k做爲自變量,返回元素的存儲下標。
  • 哈希表的基本操做:
    • insert(key,value):插入鍵值對。
    • get(key):若是存在鍵爲key的鍵值對則返回其value。
    • delete(key):刪除鍵爲key的鍵值對。

簡單哈希函數

  • 除法哈希:h(k)= k mod m
  • 乘法哈希:h(k) = floor(m(KA mod 1)) 0<A<1

哈希表Python實現

class HashTable:
    def __init__(self):
        self.size=11
        self.slots=[None]*self.size
        self.data=[None]*self.size
    def hash_function(self,key,size):
        return key%size
    def rehash(self,old_hash,size):
        return (old_hash+1)%size
    def put(self,key,data):
        hash_value=self.hash_function(key,len(self.slots))
        if self.slots[hash_value]==None:
            self.slots[hash_value]=key
            self.data[hash_value]=data
        else:
            next_slot=self.rehash(hash_value,len(self.slots))
            while self.slots[next_slot]!=None and\
                  self.slots[next_slot]!=key:
                next_slot=self.rehash(next_slot,len(self.slots))
            if self.slots[next_slot]==None:
                self.slots[next_slot]=key
                self.data[next_slot]=data
            else:
                self.data[next_slot]=data
    def get(self,key):
        start_slot=self.hash_function(key,len(self.slots))
        data=None
        stop=False
        found=False
        position=start_slot
        while self.slots[position]!=None and not found and not stop:
            if self.slots[position]==key:
                found=True
                data=self.data[position]
            else:
                position=self.rehash(position,len(self.slots))
                if position==start_slot:
                    stop=True
        return data
    def __getitem__(self,key):
        return self.get(key)
    def __setitem__(self,key,data):
        self.put(key,data)

哈希衝突

因爲哈希表的大小是有限的,而要存儲信息的數量是無限的,所以,對於任何哈希函數,都會出現兩個元素映射到同一個位置的狀況,這種狀況就叫作哈希衝突。
解決哈希衝突的方法:
開放尋址法:若是哈希函數返回的位置已經有值,則能夠向後探查新的位置來儲存這個值。

  • 線性探查:若是位置p被佔用,則探查 p+1,p+2....
  • 二次探查:若是位置p被佔用,則探查p+1**2,p-1**2,p+2**2
  • 二度哈希:有n個哈希函數,當使用第一個哈希函數h1發生衝突時,則使用h2。
  • 哈希表的快速查找能夠以空間換時間,須要保證元素個數除以數組容積小於0.5,這個比值就是裝載率。
    拉鍊法:哈希表的每一個位置都鏈接一個鏈表,當衝突發生時,衝突的元素被加到該位置鏈表的最後。
  • 拉鍊表須要保證每個鏈表的長度都不要太長。
  • 拉鍊法的裝載率是能夠大於一的。
  • 插入、查找等操做的時間複雜度是O(1)的。

哈希在python中的應用

  • 字典和集合都是經過哈希表來實現的
  • 集合能夠看做沒有value的字典,由於集合也有不重複的性質。
  • 經過哈希函數把字典的鍵映射爲函數:
dic = {'name':'cui'}
#能夠認爲是h('name')=1,則哈希表爲[None,'cui']

樹形結構

二叉樹

二叉樹的節點的節點定義

在堆排序時曾經介紹了什麼是二叉樹,當時是用列表來實現的,可是二叉樹可能出現空值,浪費空間,因此使用相似鏈表的存儲結構。

class BiTreeNode:
    def __init__(self,data):
        self.data=data
        self.lchild=None
        self.rchild=Node

二叉樹的遍歷

二叉樹的遍歷有兩類四種:

  • 深度優先:前序遍歷,中序遍歷,後序遍歷。
  • 對於有兩個遍歷求二叉樹的方法:前序找根節點(根在前面),中序找左右子樹,後序找根節點(根在後面)
#前序遍歷,root爲根節點
def pre_order(root):
    if root:
    print(root.data,end = '')
    pre_order(root.lchild)
    pre_order(root.rchild)

#中序遍歷,若是lchild沒值則出棧
def in_order(root):
    if root:
    pre_order(root.lchild)
    print(root.data,end = '')
    pre_order(root.rchild)

#後序遍歷,若是rchild沒值則出棧
def post_order(root):
    if root:
    pre_order(root.lchild)
    pre_order(root.rchild)
    print(root.data,end = '')
  • 廣度優先:層次遍歷
#根據隊列實現
def level_order(root):
    q=deque()
    q.append(root)
    while(len(q)>0):
        x=q.popleft()
        print(x.data,end='')
        if x.lchild():
            q.append(x.lchild)
        if x.rchild():
            q.append(x.rchild)

二叉搜索樹

相關知識點

二叉搜索樹,也叫二叉排序樹,它要求每個節點左子樹的節點都比它小,右子樹的節點都比他大。

  • 二叉搜索樹的遍歷是升序序列
  • 若是y是x左子樹的一個節點,那麼y.key <=x.key;
  • 若是y是x右子樹的一個節點,那麼y.key >= x.key;

二叉搜索樹的插入

class BST:
    def __init__(self):
        self.root=None  #空不是根節點 而是None
    
    def insert(self,key):
        if not self.root:
            self.root = BiTreeNode(key)
        else:
            p=self.root
            while p:
                if key < p.data:    #分爲左子樹是否爲空的狀況
                    if p.lchild:    #左子樹有節點就在左子樹繼續查找,不然就插入左節點的位置
                        p = p.lchild
                    else:
                        p.lchild = BiTreeNode(key)
                elif key > p.data:
                    if p.rchild:    
                        p = p.rchild
                    else:
                        p.lchild = BiTreeNode(key)
                        break
                else:
                    break

二叉搜索樹的查找

def query(self,key):
    p = self.root
    while p :
        if key < p.data:
            p = p.lchild
        elif key >p.data:
            p=p.rchild
        else:
            return True
    return False

二叉搜索樹的刪除

刪除有三種狀況:

  • 若是要刪除的節點是葉子節點,那麼找到後直接刪除。
  • 若是要刪除的節點有一個子節點點,將 此節點的父節點和子節點相鏈接,而後刪除此節點。
  • 若是刪除的節點有兩個子節點,找到其左子樹最大的節點或者右子樹的最小節點,刪除並替換當前節點,若最後一個一個節點還有一個右子節點,那麼再按照第二種狀況處理。

二叉搜索樹的效率和AVL樹

平均狀況下,二叉搜索時的時間複雜度爲O(logn),可是二叉搜索樹可能會出現偏斜的狀況,須要採用隨機打亂的方法,因此這時候採用AVL樹(自動平衡樹)。
相關知識點:
AVL樹:AVL樹是一棵自平衡的二叉搜索樹,它具備如下性質:

  • 根的左右子樹高度之差的絕對值不能超過1.
    • 計算方法:
    • 每一個節點的左右子樹的深度之差,也就是平衡因子。
  • 根的左右子樹都是平衡二叉樹。

AVL樹的插入操做

插入一個節點可能會形成AVL樹的不平衡,能夠經過旋轉操做來修正。
插入一個節點後,只有從插入節點到根節點的路徑上的節點的平衡可能被改變,須要找到第一個平衡條件的節點,稱之爲K,K的兩棵子樹高度差確定爲2.
不平衡的出現有四種狀況:

  • 不平衡是因爲對K的右子節點的右子樹插入致使的:左旋。
  • 不平衡是因爲對K的左子節點的左子樹插入致使的:右旋。
  • 不平衡是因爲右子節點的左子樹插入致使的:右旋->左旋。
  • 不平衡是因爲左子節點的右子樹插入致使的:左旋->右旋。

B樹

B-Tree是一種自平衡的多路搜索樹,B-Tree存儲在硬盤裏,用於數據庫的索引。

相關文章
相關標籤/搜索