数据结构与算法【入门】
复杂度
时间
用来估算算法运行时间的一个单位,越高越慢
快速判断算法复杂度
- 确定问题规模-->O(N)
- 循环减半过程-->O(logN)
- k层关于N的循环-->O(N^k)
空间
算法内存占用大小的单位
判断
- 使用了几个变量-->N
- 使用长度为n的一维列表-->O(N)
- 使用长度为n的二维列表-->O(N²)
递归
特点:
- 结束条件
- 调用自身
汉诺塔问题
n个盘子时:

-
把N-1个圆盘从A经过C移动到B
-
把第N个圆盘从A移动到C
-
把N-1个圆盘从B经过A移动到C
-
\[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()方法使用的是顺序查找
二分查找(折半查找)
查找过程如图:(可以联想数字炸弹的游戏)




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逼排序组
冒泡排序
-
列表每两个相邻的数,如果前面比后面大,则交换这两个数
-
一趟排序完成后,则无序区减少一个数,有序区增加一个数
-
代码关键点:趟,无序区和有序区的范围
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] -
时间复杂度:O(N²)
-
优化:如果一趟排序没有发生交换,则说明列表已经有序,可以直接结束
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
选择排序
-
一趟排序记录最小的数,放到第一个位置
-
再一趟记录无序区最小的数,放到第二个位置
...
-
算法关键点:有序区和无序区,无序区最小数的位置
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]
- 时间复杂度:O(N²)
插入排序
- 初始时手里(有序区)只有一张牌
- 每次(从无序区)摸一张牌,插入到手里已有的正确位置
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
- 时间复杂度:O(N²)
牛逼排序组
快速排序
- 取一个元素p(第一个元素),使元素p归位
- 列表被p分成两部分,左边比p小,右边比p大(先右后左)
- 递归完成排序

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)
-
\[\begin{array}{l} 时间复杂度:O(Nlog_2N),最坏情况:O(N²)\\ 空间复杂度:O(log_2N),最坏情况:O(N) \end{array} \]
归并排序
-
分解:将列表越分越小,直至分成一个元素
-
终止条件:一个元素有序
-
合并:将两个有序列表归并,列表越来越大

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)
-
\[\begin{array}{l} 时间复杂度:O(Nlog_2N)\\空间复杂度:O(N) \end{array} \]
-
python的内部排序是基于归并排序的(因为稳定)
堆排序⭐
- 堆是一种特殊的完全二叉树
- 大根堆:任一节点都比其子节点大
- 小根堆:任一节点都比其子节点小
- 堆的向下调整
- 当根节点的左右子树都是堆时,可以通过一次向下调整来将其编程一个堆
- 过程
- 建立堆
- 得到堆顶元素,为最大元素
- 去掉堆顶,将堆最后一个元素放到堆顶,通过一次调整重新使堆有序
- 堆顶元素为第二大元素
- 重复步骤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
-
\[时间复杂度:O(Nlog_2N) \]
-
内置模块----heapq,q->queue 优先队列
- heapify(li):将一个列表建成堆,默认小根堆
- heapop(li):弹出一个最值元素
-
Topk问题:设计算法得到前K大的数(k<n)
- 解决思路:O(nlogK)
- 取列表前k个元素建立一个小根堆。堆顶就是目前第k大的数
- 一次向后遍历原列表,对于列表中的元素,如果小于堆顶,则忽略该元素;如果大于堆顶,则将堆顶更换为该元素,并且对堆进行一次调整
- 遍历列表所有元素后,倒序弹出堆顶
- 解决思路:O(nlogK)
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)
牛逼排序的缺点:
- 快速:极端情况下排序效率低
- 归并:需要额外的内存开销
- 堆:速度相对较慢

其他排序组
希尔排序
- 分组插入排序
- 首先取一个整数d1=n/2,将元素分为d1个组,每组相应位置两元素之间距离为d1,在各组内进行直接插入排序
- 取第二个整数d2=d1/2,重复上述分组排序过程,直到di=1,即所有元素在同一组内进行直接插入排序
- 希尔排序每趟并不使默写元素有序,而是使整体数据越来越接近有序(整个列表逆序的数越来越少),最后一趟排序使得所有数据有序
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
计数排序
- 对列表进行排序,已知列表范围。
- 时间复杂度为O(N),空间复杂度为O(2N)
- 但是限制很多,范围大了很费内存
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)
基数排序
-
多关键字排序,比如先按照年龄排序,再按照薪资排序
-
一般看数的个位、十位....
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 -
时间复杂度: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])
数据结构
线性结构、树结构、图结构
列表/数组
- 列表是顺序存储的
- 32位机器上,一个整数占4字节,一个地址也占4字节
- 数组与列表两点不同
- 数组中元素类型要相同,列表中存的是地址
- 数组长度固定
- 基本操作:按下标查找(O(1))、插入(O(n))、删除(O(n))....
栈
- 只能在一端进行插入或删除操作的列表
- 后进先出 LIFO(last-in,first-out)
- 概念:栈顶、栈底
- 基本操作
- 进栈(压栈):push,li.append
- 出栈:pop,li.pop
- 取栈顶: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()
队列
-
仅允许列表的一端(rear)进行插入,另一端(front)进行删除
-
插入的一端称为队尾,动作称为进队入队
-
删除的一端称为队头,动作称为出队
-
-
先进先出(FIFO,first-in,first-out)
-
环形队列
-
当队尾指针rear == Maxsize +1时,再前进一个位置就自动到0
-
队首指针前进1:front = (front + 1 )% MaxSize
-
队尾指针前进1:rear = (rear + 1 )% MaxSize
-
队空条件:rear == front
-
队满条件:(rear+1) % Maxsize == front

''' 此代码只为理解,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 -
-
双向队列
- 队首进出队
- 队尾进出队
- 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
迷宫问题

-
回溯法
-
思路:从一个节点开始,任意找下一个能走的点,当找不到能走的点时,退回上一个点寻找是否有其他方向的点
-
使用栈存储当前路径(深度优先搜索)(但是路径不是最短的)
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
-
-
使用队列存储当前正在考虑的节点(广度优先搜索)
-
从一个节点开始,寻找所有接下来能继续走的点,继续不断寻找,直到找到出口
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
-
链表
- 由一系列节点组成的元素结合。
- 每个节点包含两个部分
- 数据域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
插入
-
时间复杂度O(1)
p.next = curNode.next curNode.next= p
删除
-
时间复杂度O(1)
p = curNode.next curNode.next=curNode.next.next del p
双链表

class Node:
def __init__(self,item):
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
总结
- 链表的删除和插入比列表快
- 链表的内存可以更灵活的分配
- 链表的树和图的结构有很大的启发性
哈希表
-
通过哈希函数来计算数据存储位置的数据结构(直接寻址标+哈希函数)
- insert(key,value)
- get(key)
- delete(key)
-
哈希函数

-
哈希冲突:哈希表的大小是有限的,会出现两个不同映射到同一个位置上的情况,比如h(k)= k%7,h(0)=h(7)=h(14)...
-
解决方式
- 开放寻址法:若返回的位置已经有值,则可以向后探查新的位置来存储这个值
- 线性探查:如果位置i被占用,则探查i+1,i+2...
- 二次探查:如果位置i被占用,则探查i+1²,i-1²,i+2²,i-2²...
- 二度哈希:有n个哈希函数,当使用第1个哈希函数h1发生冲突时,则尝试使用h2,h3...
- 拉链法:哈希表每个位置都连接一个链表,当冲突发生时,冲突的元素将被加到该位置链表的最后

- 开放寻址法:若返回的位置已经有值,则可以向后探查新的位置来存储这个值
-
常见哈希函数

-
应用
- 集合
- 字典
- MD5、SHA2
- 文件的哈希值
- 验证下载的文件是否完整
- 云存储服务可以利用它来判断用户上传的文件是否存在于服务器上,从而实现秒传的功能
- 文件的哈希值
-
树
'''
模拟文件系统
'''
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
-
遍历
- 前序:根左右
- 中序:左根右
- 后序:左右根
- 层次:队列
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)
二叉搜索树⭐

左子树所有节点都比根小,右子树所有节点都比根大
二叉树的中序序列是有序的
平均时间复杂度为:O(logN)
最坏情况:二叉搜索树可能非常偏斜

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树⭐

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

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

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

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

-
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
扩展应用
- B树(B-Tree):自平衡的多路搜索树。常用于数据库的索引
- B+树

浙公网安备 33010602011771号