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


什么是數據結構

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

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

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

數據結構的分類

根據邏輯結構划分:

  • 線性結構:數據結構中的元素一對一的關系,一前驅,一后繼。
  • 樹結構:數據結構中元素一對多的關系,一前驅,多后繼。
  • 圖結構:數據結構中元素存在多對多的關系,多前驅,多后繼,我也不會。
    • 判斷一個圖形能不能一筆畫完,就判斷它的奇數度節點數目是否為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)操作復雜度過高,不采用。

  • 由於數組定長,不能繼續添加數據,如果是列表,出隊的操作就會出現空位,所以想辦法讓數組變成一個圓環。

  • 設置兩個指針,隊首指針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存儲在硬盤里,用於數據庫的索引。


免責聲明!

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



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