数据结构与算法【入门】

复杂度

时间

用来估算算法运行时间的一个单位,越高越慢

\[O(1)<O(log_2{N})<O(N)<O(Nlog_2{N})<O(N²)<O(N²log_2{N})<O(N³) \]

快速判断算法复杂度

  1. 确定问题规模-->O(N)
  2. 循环减半过程-->O(logN)
  3. k层关于N的循环-->O(N^k)

空间

算法内存占用大小的单位

判断

  1. 使用了几个变量-->N
  2. 使用长度为n的一维列表-->O(N)
  3. 使用长度为n的二维列表-->O(N²)

递归

特点:

  1. 结束条件
  2. 调用自身

汉诺塔问题

n个盘子时:

image

  1. 把N-1个圆盘从A经过C移动到B

  2. N个圆盘从A移动到C

  3. 把N-1个圆盘从B经过A移动到C

  4. \[f(x)=2f(x-1)+1 \]

def hanoi(n,a,b,c):
    if n>0:
        hanoi(n-1,a,c,b)
        print("moving from %s to %s" % (a,c))
        hanoi(n-1,b,a,c)
        
hanoi(3,'A','B','C')
        

查找

顺序查找(线性查找)

从第一个元素到最后一个元素,时间复杂度O(n),空间复杂度O(n)

def linear_search(arr,key):
    for index,val in enumerate(arr):
        if val == key:
            return index
        else:
            return None

python内置的index()方法使用的是顺序查找

二分查找(折半查找)

查找过程如图:(可以联想数字炸弹的游戏)

image

image

image

image

def binary_search(arr,ley):
    left = 0
    rihgt = len(arr)-1
    while left <= right:	# 候选区有值
        mid = (left+right)//2
        if arr[mid]==key:
            return mid
        elif arr[mid] > key:	# 待查找的值在mid的左侧
            right = mid - 1
        else:	# 待查找的值在mid的右侧
            left = mid + 1
    else:
        return None

因为查找规模会再每一次遍历减半,所以时间复杂度是O(logN),空间复杂度是O(N)

为什么内置的index()不是使用二分查找呢?
因为二分查找的前提是序列是有序的,而排序也消耗性能

排序

Low逼排序组

冒泡排序

  1. 列表每两个相邻的数,如果前面比后面大,则交换这两个数

  2. 一趟排序完成后,则无序区减少一个数,有序区增加一个数

  3. 代码关键点:趟,无序区和有序区的范围

    def bubble_sort(li):
        for i in range(len(li)-1):
            for j in range(len(li)-1-i):
                if li[j]>li[j+1]:
                    li[j+1],li[j]=li[j],li[j+1]
    
  4. 时间复杂度:O(N²)

  5. 优化:如果一趟排序没有发生交换,则说明列表已经有序,可以直接结束

    def bubble_sort(li):
        for i in range(len(li)-1):
            exchange = False
            for j in range(len(li)-i-1):
                if li[j+1]>li[j]:
                    li[j+1],li[j]=li[j],li[j+1]
                    exchange = True
            if not exchange:
                return
    

选择排序

  1. 一趟排序记录最小的数,放到第一个位置

  2. 再一趟记录无序区最小的数,放到第二个位置

    ...

  3. 算法关键点:有序区和无序区,无序区最小数的位置

def select_sort(li):
    for i in range(len(li)-1):
        min_index = i
        for j in range(i+1,len(li)):
        	if li[j]<li[min_index]:
				min_index = j        
        li[i],li[min_index] = li[min_index],li[i]        
  1. 时间复杂度:O(N²)

插入排序

  1. 初始时手里(有序区)只有一张牌
  2. 每次(从无序区)摸一张牌,插入到手里已有的正确位置
def insert_sort(li):
    for i in range(1,len(i)):
        tmp = li[i]
        j = i-1
        while li[j]<tmp and j>=0:
            li[j+1]=li[j]
            j -= 1
        # 循环结束后,j指向了插入前一位的位置
        li[j+1] = tmp
  1. 时间复杂度:O(N²)

牛逼排序组

快速排序

  1. 取一个元素p(第一个元素),使元素p归位
  2. 列表被p分成两部分,左边比p小,右边比p大(先右后左)
  3. 递归完成排序

image

def partition(li,left,right):
	tmp = li[left]
    while left<right:
    	while left < right and li[right] >= tmp:
            right -= 1
        li[left] = li[right]
        while left < right and li[left] <= tmp:
            left += 1
        li[right] = li[left]
    li[left] = tmp
	return left
def quick_sort(li,left,right):
    if left < right:
        mid = partition(li,left,right)
        quick_sort(li,left,mid-1)
        quick_sor(li,mid+1,right)	
  1. \[\begin{array}{l} 时间复杂度:O(Nlog_2N),最坏情况:O(N²)\\ 空间复杂度:O(log_2N),最坏情况:O(N) \end{array} \]

归并排序

  1. 分解:将列表越分越小,直至分成一个元素

  2. 终止条件:一个元素有序

  3. 合并:将两个有序列表归并,列表越来越大

    image

def merge(li,low,mid,high):
    i = low
    j = mid+1
    ltmp = []
    while i<=mid and j<=h:	# 只要左半部分和右半部分都有数
        if li[i] < li[j]:
            ltmp.append(li[i])
         	i+=1
         else:
            ltmp.append(li[j])
            j+=1
     # 将li剩余的数添加到ltmp中
     while i<=mid:
        ltmp.append(li[i])
        i+=1
     while j <= high:
        ltmp.append(li[j])
        j+=1
     # ltmp覆盖li
     li[low:high+1] = ltmp

def merge_sort(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)
  1. \[\begin{array}{l} 时间复杂度:O(Nlog_2N)\\空间复杂度:O(N) \end{array} \]

  2. python的内部排序是基于归并排序的(因为稳定)

堆排序⭐

  1. 堆是一种特殊的完全二叉树
    • 大根堆:任一节点都比其子节点大
    • 小根堆:任一节点都比其子节点小
  2. 堆的向下调整
    • 当根节点的左右子树都是堆时,可以通过一次向下调整来将其编程一个堆
  3. 过程
    1. 建立堆
    2. 得到堆顶元素,为最大元素
    3. 去掉堆顶,将堆最后一个元素放到堆顶,通过一次调整重新使堆有序
    4. 堆顶元素为第二大元素
    5. 重复步骤3,直到堆变为空
def sift(li,low,high):
"""
	li:列表
	low:堆的根节点位置
	high:堆的最后一个元素的位置
"""
	i = low		# i最开始指向根节点
    j = 2*i+1	# j最开始是左孩子
    tmp= li[low]	# 把根节点值存起来
    while j<=high:
        if j+1<=high and li[j+1]>li[j]:	# 如果右孩子有并且比左孩子大
            j = j+1 	# j指向右孩子
        if li[j]>tmp:
            li[i] = li[j]
            # 往下一层看
            i = j	
            j = 2*i+1
        else:		# tmp 更大,把tmp放到i的位置上
            li[i] = tmp	# 把tmp放到某一层上
            break
     else:
        li[i] = tmp # 把tmp放到叶子节点上
        
def heap_sort(li):
    n = len(li)
    # i从最后一个叶子节点的根开始
    for i in range((n-2)//2,-1,-1):
       sift(li,i,n-1)
    for i in range(n-1,-1,-1):
        # i 指向当前堆的最后一个元素
        li[0],li[i] = li[i],li[0]
        sift(li,0,i-1)	# i-1是新的high
  1. \[时间复杂度:O(Nlog_2N) \]

  2. 内置模块----heapq,q->queue 优先队列

    • heapify(li):将一个列表建成堆,默认小根堆
    • heapop(li):弹出一个最值元素
  3. Topk问题:设计算法得到前K大的数(k<n)

    • 解决思路:O(nlogK)
      1. 取列表前k个元素建立一个小根堆。堆顶就是目前第k大的数
      2. 一次向后遍历原列表,对于列表中的元素,如果小于堆顶,则忽略该元素;如果大于堆顶,则将堆顶更换为该元素,并且对堆进行一次调整
      3. 遍历列表所有元素后,倒序弹出堆顶
def sift(li,low,high):
    '''
    	调整堆
    '''
	i = low		# i最开始指向根节点
    j = 2*i+1	# j最开始是左孩子
    tmp= li[low]	# 把根节点值存起来
    while j<=high:
        if j+1<=high and li[j+1]<li[j]:	
            j = j+1 	
         if li[j]<tmp:
            li[i] = li[j]
            i = j	
            j = 2*i+1
         else:		
            break
    li[i] = tmp # 把tmp放到叶子节点上

def top(li,k):
    heap = li[0:k]
    # 建堆
    for i in range((k-2)//2,-1,-1):
        sift(heap,i,k-1)
    # 遍历li剩余的数   
    for i in range(k,len(li)-1):
        if li[i] > heap[0]:
            heap[0]=li[i]
            sift(heap,0,k-1)
    # 最后倒序出数
    for i in range(k-1,-1,-1):
        heap[0],heap[i] = heap[i],heap[0]
        sift(heap,0,i-1)
    return heap   

小结

low逼排序:冒泡、插入、选择

牛逼排序:快速<归并<堆,时间复杂度都是O(nlogn)

牛逼排序的缺点:

  1. 快速:极端情况下排序效率低
  2. 归并:需要额外的内存开销
  3. 堆:速度相对较慢

image

其他排序组

希尔排序

  1. 分组插入排序
  2. 首先取一个整数d1=n/2,将元素分为d1个组,每组相应位置两元素之间距离为d1,在各组内进行直接插入排序
  3. 取第二个整数d2=d1/2,重复上述分组排序过程,直到di=1,即所有元素在同一组内进行直接插入排序
  4. 希尔排序每趟并不使默写元素有序,而是使整体数据越来越接近有序(整个列表逆序的数越来越少),最后一趟排序使得所有数据有序
def insert_sort_gap(li,gap):
    for i in range(gap,len(li)):
        tmp = li[i]
        j = i-gap
        while j >= 0 and li[j] > tmp:
            li[j+gap] = li[j]
            j-=gap
        li[j+gap] = tmp
def shell_sort(li):
    d = len(li) // 2
    while d>=1: 
    	inser_sort_gap(li,d)
    	d //= 2
        

计数排序

  1. 对列表进行排序,已知列表范围
  2. 时间复杂度为O(N),空间复杂度为O(2N)
  3. 但是限制很多,范围大了很费内存
def count_sort(li,max_count=100):
    count = [0] * max_count
    for val in li:
        count[val] += 1
    li.clear()	# 清空li数组
    # 这里的时间复杂度是O(N)
    for index,val in enumerate(count):
        for i in range(val):
            li.append(index)

桶排序

从计数排序衍生出来的,可优化针对耗内存的问题

将元素分在不同的桶中,再对每个桶中的元素排序

def bucket_sort(li,n=100,max_num=10000):
    buckets = [[] for _ in range(n)]	# 创建桶
    for var in li:
        i = min(var // (max_num // n),n-1) # i表示var放到几号桶里
        buckets[i].append(var)	# 把var加到桶里
        # 保持桶内的排序
    	for j in range(len(bucket[i])-1,0,-1):
            if buckets[i][j] < buckets[i][j-1]:
                buckets[i][j],buckets[i][j-1] = buckets[i][j-1],buckets[i][j]
            else:
                break
    sorted_li = []
    for buc in buckets:
        sorted_li.extend(buc)
    return sorted_li    
        

时间复杂度:O(N+k),最坏:O(N²+k)

空间复杂度:O(N+k)

基数排序

  1. 多关键字排序,比如先按照年龄排序,再按照薪资排序

  2. 一般看数的个位、十位....

    def radix_sort(li):
        max_num = max(li)	# 最大值,确定位数是多少就做几次循环
        it = 0
        while 10 ** it <= max_num:
            # 建桶
            buckets = [[] for _ in range(10)]
            # 分桶
            for var in li:
                digit = (var // (10 ** it) % 10)
                buckets[digit].append(var)
            # 把数重写回li
            li.clear()
            for buc in buckets:
                li.extends(buc)
                
            it += 1
    
  3. 时间复杂度:O(kN),空间复杂度:O(k+N)

练习题

"""
s = 'anagram',t='nagaram',return True
s = 'rat',t='cat',return False
"""
def isAnagram(self,s,t):
    dict1 = {}
    dict2 = {}
    for i in s:
 		dict1[ch] = dict1.get(ch,0) + 1
    for ch in t:
        dict2[ch] = dict2.get(ch,0) + 1
        
    return dict1 == dict2
"""
给定一个m*n的矩阵,查找一个数是否存在
每一行的列表是有序的
下一行的比上一行的都大
"""
def searchMatrix(matrix,target):
    h = len(matrix)
    if h == 0:
        return False
    w = len(matrix[0])
    if w == 0:
        return False
    left = 0
    right = w * h -1
    while left <= right:
        mid = (left + right) // 2
        i = mid // w
        if matrix[mid] == target:
            return True
        elif matrix[mid] > target:
            right = mid - 1
        else:
            left = mid + 1
     else:
        return False
"""
给定一个列表和一个整数,设计算法找到两个数的下标,使得两个数之和为给定的整数。保证肯定仅有一个结果
例如:[1,2,4,5],输入整数3,返回(0,1)
""" 
# 二分查找
def binary_search(arr,left,right,key):
    while left <= right:	
        mid = (left+right)//2
        if arr[mid][0]==key:
            return mid
        elif arr[mid][0] > key:	
            right = mid - 1
        else:	
            left = mid + 1
    else:
        return None

def two_sum(nums,target):
    new_nums = [[num,i] for i,num in enumerate(nums)]
    new_nums.sort(key=lambda x:x[0])
    n = len(new_nums)
    for i in range(n):
        a = new_nums[i][0]
        b = target-a
        if b>=a:	# 因为已经排序,比a大则在右半部分找,比a小则在左半部分找
            j = binary_search(new_nums,i+1,n-1,b)
        else:
            j = binary_search(new+nums,0,i-1,b)
        if j:
            break
    return sorted(new_nums[i][1],new_nums[j][1])	

数据结构

线性结构、树结构、图结构

列表/数组

  1. 列表是顺序存储的
    1. 32位机器上,一个整数占4字节,一个地址也占4字节
    2. 数组与列表两点不同
      • 数组中元素类型要相同,列表中存的是地址
      • 数组长度固定
  2. 基本操作:按下标查找(O(1))、插入(O(n))、删除(O(n))....

  1. 只能在一端进行插入或删除操作的列表
  2. 后进先出 LIFO(last-in,first-out)
  3. 概念:栈顶、栈底
  4. 基本操作
    1. 进栈(压栈):push,li.append
    2. 出栈:pop,li.pop
    3. 取栈顶:get_top,li[-1]
class Stack:
    def __init__(self):
        self.stack = []
    def push(self,element):
        self.stack.append(element)
    def pop(self):
        return self.stack.pop()
    def get_top(self):
        if len(self.stack)>0:
            return self.stack[-1]
        else:
            return None
    def is_empty:
        return len(self.stack) == 0
'''
括号匹配问题
'''
def brace_match(s):
    match = {'}':'{',']':'[',')':'('}
    stack = Stack()
    for ch in s:
        if ch in {'(','[','{'}:
            stack.push(ch)
        else:
            if stack.is_empty():
                return False
            elif stack.get_top == match[ch]:
                stack.pop()
            else:
                return False
     return stack.is_empty()

队列

  1. 仅允许列表的一端(rear)进行插入,另一端(front)进行删除

    • 插入的一端称为队尾,动作称为进队入队

    • 删除的一端称为队头,动作称为出队

  2. 先进先出(FIFO,first-in,first-out)

  3. 环形队列

    • 当队尾指针rear == Maxsize +1时,再前进一个位置就自动到0

    • 队首指针前进1:front = (front + 1 )% MaxSize

    • 队尾指针前进1:rear = (rear + 1 )% MaxSize

    • 队空条件:rear == front

    • 队满条件:(rear+1) % Maxsize == front

      image

    '''
    此代码只为理解,python有现成的可以用
    from collections import deque	双向队列
    '''
    class Queue:
        def __init__(self,size=100):
            self.queue =  [0 for in range(size)]
            self.front = 0
            self.rear = 0
            self.size = size 
        def push(self,element):
            if not self.is_full():
            	self.rear = (self.rear+1) % self.size
            	self.queue[self.rear] = element
            else:
                raise IndexError("Queue is filled")
        def pop(self):
            if not self.is_empty():
    	        self.front = (self.front+1) % self.size
            else:
                raise IndeError("Queue is Empty")
            return self.queue[self.front]
        def is_empty(self):
            return self.rear == self.front:
        def is_full(self):
            return (self.rear+1)%self.size == self.front  
    
  4. 双向队列

    1. 队首进出队
    2. 队尾进出队
    3. python内存的队模块
    # import queue 不使用这个是因为它是控制线程的
    from collection import deuqe
    
    q = deque([4,5,6],5)	# 堆满的话前面的会自动出列
    q.append(1) # 队尾进队
    q.popleft() # 队首出队
    q.appendleft(1) # 队首进队
    q.pop()	#队尾出队
    
    '''
    使用队列实现linux的tail
    '''
    def tail(n):
        with optn('xxx.txt','r') as f:
            q = deque(f,n)
            return q
    

迷宫问题

image

  1. 回溯法

    1. 思路:从一个节点开始,任意找下一个能走的点,当找不到能走的点时,退回上一个点寻找是否有其他方向的点

    2. 使用栈存储当前路径(深度优先搜索)(但是路径不是最短的)

      maze = [
          [1,1,1,1,1,1,1,1,1,1],
          [1,0,0,1,0,0,0,1,0,1],
          [1,0,0,1,0,0,0,1,0,1],
          [1,0,0,0,0,1,1,0,0,1],
          [1,0,1,1,1,0,0,0,0,1],
          [1,0,0,0,1,0,0,0,0,1],
          [1,0,1,0,0,0,1,0,0,1],
          [1.0,1,1,1,0,1,1,0,1]
          [1,1,0,0,0,0,0,0,0,1],
          [1,1,1,1,1,1,1,1,1,1],
      ]
      # 方向组 
      dirs = [
          lambda x,y:(x+1,y),
          lambda x,y:(x-1,y),
          lambda x,y:(x,y+1),
          lambda x,y:(x,y-1),
      ]
      def maze_path(x1,y1,x2,y2): # x1,y1是起始坐标,x2,y2是终点坐标
          stack = []
          stack.append((x1,y1))	# 加入起点
          while(len(stack)>0):
              curNode = stack[-1] # 当前节点
              if curNode[0] == x2 and curNode[1] == y2:	# 走到终点时,打印
              	for p in stack:
                      print(p)
                  return True
              # x,y四个方向:上:x-1,下:x+1,左:y-1,右:y+1
              for dir in dirs:
                  nextNode = dir(curNode[0],curNode[1])
                  if maze[nextNode[0]][nextNode[1]] == 0:
                      stack.append(nextNode)
                      maze[nextNode[0]][nextNode[1]] = 2 # 2 表示为已经走过
                      break
              else:
                  maze[nextNode[0]][nextNode[1]] = 2
                  stack.pop()
          else:
              print("没有路")
              return False
      
  2. 使用队列存储当前正在考虑的节点(广度优先搜索)

    1. 从一个节点开始,寻找所有接下来能继续走的点,继续不断寻找,直到找到出口

      from collection import deque
      maze = [
          [1,1,1,1,1,1,1,1,1,1],
          [1,0,0,1,0,0,0,1,0,1],
          [1,0,0,1,0,0,0,1,0,1],
          [1,0,0,0,0,1,1,0,0,1],
          [1,0,1,1,1,0,0,0,0,1],
          [1,0,0,0,1,0,0,0,0,1],
          [1,0,1,0,0,0,1,0,0,1],
          [1.0,1,1,1,0,1,1,0,1]
          [1,1,0,0,0,0,0,0,0,1],
          [1,1,1,1,1,1,1,1,1,1],
      ]
      dirs = [
          lambda x,y:(x+1,y),
          lambda x,y:(x-1,y),
          lambda x,y:(x,y+1),
          lambda x,y:(x,y-1),
      ]
      def print_r(path):
          curNode = path[-1]
          realpath = []
          while curNode[2] == -1:
              realpath.append(curNode[0:2])
              curNode = path[curNode[2]]
          realpath.append(curNode[0:2]) # 放起点
          realpath.reverse()
          for node in realpath:
              print(node)
          
      def maze_patch_queue(x1,y1,x2,y2):
          queue = deque()
          queue.append((x1,y1),-1)
          path = []
          while len(queue) > 0:
              curNode = queue.pop()
              path.append(curNode)	
              if curNode[0] == x2 and curNode[0] == y2:	# 到达终点
                  print_r(path)
                  return True
              for dir in dirs:
                  nextNode = dir(curNode[0],curNode[1])
                  if maze[nextNode[0]][nextNode[1]] == 0:
                      queue.append((nextNode[0],nextNode[0]),len(path)-1)	# 后续节点进队,记录哪个节点带它来的
                      maze[nextNode[0]][nextNode[1]] = 2	# 标记已经走过
          else:
              print("没有路")
              return False
      

链表

  1. 由一系列节点组成的元素结合。
  2. 每个节点包含两个部分
    • 数据域item
    • 下个节点的指针next

创建链表

class Node:
    def __init__(self,item):
        self.item = item
        self.next = None
 	
def create_linklist_head(li):
    '''
    头插法
    '''
    head = Node(li[0])
    for element in li[1:]:
        node = Node(element)
       	node.next = head
        head = node
	return head

def create_linklist_tail(li)        
    '''
    尾插法
    '''  
    head = Node(li[0])
    tail = head
    for element in li[1:]:
        node = Node(element)
        tail.next = node
        tail = node
    return head
    
def print_linklist(lk):
    while lk:
        print(lk.item,end=',')
        lk = lk.next

插入

  1. 时间复杂度O(1)

    p.next = curNode.next
    curNode.next= p
    

删除

  1. 时间复杂度O(1)

    p = curNode.next
    curNode.next=curNode.next.next
    del p
    

双链表

image

class Node:
    def __init__(self,item):
        self.item = item
        self.next = None
        self.prior = None
  1. 插入

    p.next=curNode.next
    curNode.next.prior = p
    p.prior = curNode
    curNode.next = p
    
  2. 删除

    p=curNode.next
    curNode.next = p.next
    p.next.prior = curNode
    del p
    

总结

  1. 链表的删除和插入比列表快
  2. 链表的内存可以更灵活的分配
  3. 链表的树和图的结构有很大的启发性

哈希表

  1. 通过哈希函数来计算数据存储位置的数据结构(直接寻址标+哈希函数)

    • insert(key,value)
    • get(key)
    • delete(key)
  2. 哈希函数

    image

    1. 哈希冲突:哈希表的大小是有限的,会出现两个不同映射到同一个位置上的情况,比如h(k)= k%7,h(0)=h(7)=h(14)...

    2. 解决方式

      1. 开放寻址法:若返回的位置已经有值,则可以向后探查新的位置来存储这个值
        1. 线性探查:如果位置i被占用,则探查i+1,i+2...
        2. 二次探查:如果位置i被占用,则探查i+1²,i-1²,i+2²,i-2²...
        3. 二度哈希:有n个哈希函数,当使用第1个哈希函数h1发生冲突时,则尝试使用h2,h3...
      2. 拉链法:哈希表每个位置都连接一个链表,当冲突发生时,冲突的元素将被加到该位置链表的最后image
    3. 常见哈希函数

      image

    4. 应用

      1. 集合
      2. 字典
      3. MD5、SHA2
        1. 文件的哈希值
          • 验证下载的文件是否完整
          • 云存储服务可以利用它来判断用户上传的文件是否存在于服务器上,从而实现秒传的功能

'''
模拟文件系统
'''
class Node:
    def __init__(self,name,type='dir'):
        self.name = name
        self.type = type
        self.children = []
 		self.parent = None  
     
    def __repr__(self):
        return self.name
    
class FileSystemTree:
    def __init__(self):
		self.root = Node("/")
        self.now = self.root
        
    def mkdir(self,name):
        if name[-1] != "/":
            name += "/"
            
        node = Node(name)
        self.now.children.append(node)
        node.parent = self.now
        
    def ls(self):
        return self.now.children
    
    def cd(self,name):
        if name[-1] != "/":
            name += "/"
        if name == '..':
            self.now = self.now.parent
            return 
        for child in self.now.children:
			if child.name == name:
                self.now = child
                return 
        raise ValueError("invalid dir")
        

二叉树

度不超过二的树

class BiTreeNode:
    def __init__(self,data):
        self.data= data
        self.lchild = None
        self.rchild = None
  1. 遍历

    • 前序:根左右
    • 中序:左根右
    • 后序:左右根
    • 层次:队列
    from collection import deque
    def pre_oreder(root):
        if root:
            print(roor.data,end=',')
            pre_order(root.lchild)
            pre_order(root.rchild)
    def in_order(root):
        if root:
            pre_order(root.lchild)
            print(roor.data,end=',')
            pre_order(root.rchild)
    def post_order(root):
    	if root:
            pre_order(root.lchild)
            pre_order(root.rchild)
            print(roor.data,end=',')
            
    def level_order(roor):	# 广度优先
        queue = deque()
        queue.append(root)
        while len(queue) > 0:
            node = queue.popleft()
            print(node.data,end=',')
            if node.lchild:
                queue.append(node.lchild)
            if node.rchild:
                queue.apend(node.rchild)           
    

二叉搜索树⭐

image

左子树所有节点都比根小,右子树所有节点都比根大

二叉树的中序序列是有序的

平均时间复杂度为:O(logN)

最坏情况:二叉搜索树可能非常偏斜

image

class BiTreeNode:	# 树节点
    def __init__(self,data):
        self.data= data
        self.lchild = None
        self.rchild = None
    	self.parent = None    
class BST:	# 二叉搜索树
    def __init__(self,li):
        self.root = None
        self.li = li
        
    def insert(self,node,val):	# 插入
        if not node:
            node = BiTreeNode(val)
        elif val < node.data:
            node.lchild = self.insert(node.lchild,val) 
            node.lchild.parent = node
        elif val > node.data:
            node.rchild = self.insert(node.rchild,val)
			node.rchild.parent = node
        return node
    
    def query(self,node,val):	# 搜索
        if not node:
            return None
        if val < node.data:
            return self.query(node.lchild,val)
        elif val > node.data:
            return self.query(node.lrchild,val)
        else:
            return node
    
    def __remove_node_1(self,node):	# 情况1:node是叶子节点
        if not node.parent:
            self.root = None
        if node == node.parent.lchild:	# node是它父亲的左孩子
            node.parent.lchild = None
        else:
            node.parent.rchild = None
     
    
	def __remove_node_21(self.node): # 情况21:node只有一个左孩子        
		if not node.parent:
            self.root = node.lchild
        	node.lchild.parent = None
        elif node == node.parent.lchild:
            node.parent.lchild = node.lchild
            node.lchild.parent = node.parent
        else:
    		node.parent.rchild = node.lchild
            node.lchild.parent = node.parent
            
     def __remove_node_22(self.node): # 情况22:node只有一个右孩子        
        if not node.parent:
            self.root = node.rchild
        	node.lchild.parent = None
        elif node == node.parent.rchild:
            node.parent.lchild = node.rchild
            node.rchild.parent = node.parent
        else:
    		node.parent.rchild = node.rchild
            node.rchild.parent = node.parent
            
            
    def delete(self,val):	# 删除
      if self.root:
        node = self.query(val)
        if not node:
            return False
        if not node.lchild and not node.rchild:	# 叶子节点
            self.__remove_node_1(node)
        elif not node.rchild: # 只有左孩子   
           	self.__remove_node21(node)
        elif not node.lchild: # 只有右孩子
            self.__remove_node22(node)
        else:	# 两个孩子都有
            min_node = node.rchild
            while min_node.lchild:
                min_node = min_node.lchild
            node.data = min_node.data
            # 删除min_node
            if min_node.rchild:
                self.__remove_node_22(min_node)
            else:
                self.__remove_node_1(min_node)

AVL树⭐

image

一棵自平衡的二叉搜索树

  • 左右子树的高度差的绝对值不能超过1
  • 左右子树都是平衡二叉树

插入

  1. 插入一个节点可能会破坏AVL树的平衡,可以通过旋转操作进行修正
  2. 我们需要找出第一个破坏了平衡条件的节点,称之为K。K的两棵子树的高度差为2
  3. 不平衡的出现可能有4种情况
    1. 由对K的右孩子的右子树插入导致的:左旋image

    2. 由对K的左孩子的左子树插入导致的:右旋image

    3. 由对K的右孩子的左子树插入导致的:右旋-左旋image

    4. 由对K的左孩子的右子树插入导致的:左旋-右旋image

class AVLNode(BiTreeNode):
    def __init__(self,node):
        BiTreeNode.__init__(self.data)
        self.bf = 0	# bf 为 balance factor
        
class AVLTree(BST):
    def __init__(self,li=None):
        BST.__init__(self,li)
    
    def rotate_left(self,p,c):# 左孩子的左子树
		s2 = c.lchild
        p.rchild = s2
        if s2:
            s2.parent = p
        c.lchild = p
        p.parent = c
        
        p.bf = 0
        c.bf = 0
        return c
    
    def rotate_right(self,p,c):# 右孩子右子树
		s2 = c.rchild
        p.rchild = s2
        if s2:
            s2.parent = p
        c.rchild = p
        p.parent = c
        
        p.bf = 0
        c.bf = 0
        return c
        
   	def rotate_right_left(self,p,c):# 右孩子的左子树
        g = c.lchild
        
        # 右旋
        s3 = g.rchild
        c.lchild = s3
        if s3:
            s3.parent = c
        g.rchild = c
        c.parent = g
        # 左旋
        s2 = g.lchild
        p.rchild = s2
        if s2:
            s2.parent = p
        p.lchild = p
        p.parent = g
        
        # 更新bf
        if g.bf > 0:
            p.bf = -1
            c.bf = 0
        elif g.bf < 0:
            p.bf = 0
            c.bf = 1
        else:	# 插入的是g
            p.bf = 0
            c.bf = 0
        return g    
   
	def rotate_left_right(self,p,c):#左孩子的右子树
        g = r.child
        
        # 左旋
        s2 = g.lchild
        c.rchild = s2
        if s2:
            s2.parent =c
        g.lchild = c
        c.parent = g
        
        # 右旋
        s3 = g.rchild
        p.lchild = s3
        if s3:
            s3.parent = p
        p.rchild = p
        p.parent = g
        
        # 更新bf
        if g.bf < 0:
    		p.bf = 1
            c.bf = 0
        elif g.bf > 0:
			p.bf = 0 
            c.bf = -1
        else:
            p.bf = 0
            c.bf = 0
        return g
    
    def insert_no_rec(self,val):
        # 和BST一样先插入
        p = self.root
        if not p:
            self.root = BiTreeNode(val)
            return 
        while True:
            if val < p.data:
                if p.lchild:
                    p= p.lchild
                else:
                    p.lchild = BiTreeNode(val)
                    p.lchild.parent = p
                    node = p.lchild	# 存储的是插入的节点
                    break
            elif val > p.data:
                if p.rchild:
                    p = p.rchild
                else:
                    p.rchild = BiTreeNode(val)
                    p.rchild.parent = p
                    node = p.rchild
                    break
            else:
                return 
        
        # 更新balance factor
        while node.parent:
            if node.parent.lchild == node:
                # 更新node.parent的bf-=1
                if node.parent.bf < 0:	# 原来node.parent.bf == -1,更新后变成-2
                    g = node.parent.parent # 为了连接旋转之后的子树
                    x = node.parent # 旋转前的子树的根
                    if node.bf > 0:
                        n=self.rotate_left_right(node.parent,node)
            else:
                n=self.rotate_right(node.parent,node)
                # 记得:把n和g连起来
                elif node.parent.bf > 0: # 原来node.parent.bf = 1,更新之后变成0
                    node.parent.bf = 0
                    break
                else: # 原来node.parent.bf = 0,更新之后变成-1
                    node.parent.bg = -1
                    node = node.parent
                    continue
            else:	# 原来node.parent.bf == -1,更新后变成-2
                # 更新node.parent.bf += 1
                if node.parent.bf > 0:
                    g = node.parent.parent # 为了连接旋转之后的子树
                    x = node.parent # 旋转前的子树的根
                    if node.bf < 0: # node.bf = 1
                        n = self.rotate_right_left(node.parent,node)
                    else:	# node.bf = -1
                        n = self.rotate_left(node.parent,node)
                    # 记得连起来 
                elif node.parent.bf < 0: # 原来node.parent.bf = -1,更新之后变成0
                    node.parent.bf = 0
                else:
                    node.parent.bf = 1
                    node = node.parent
                    continue
            # 连接旋转后的子树
            n.parent = g
            if g:
                if x == g.lchild:
                    g.lchild = n
                else:
                    g.rchild = n
                break
            else:
                self.root = n
                break

扩展应用

  1. B树(B-Tree):自平衡的多路搜索树。常用于数据库的索引
  2. B+树
posted @ 2021-10-25 22:30  注入灵魂  阅读(136)  评论(0)    收藏  举报