数据结构入门
数据结构介绍
数据结构是指相互之间存在着一种或多种关系的数据元素的集合和该集合中数据元素之间的关系组成。
简单来说,数据结构就是设计数据以何种方式组织并存储在计算机中。
比如:列表、集合与字典等都是一种数据结构。
N.Wirth:“程序=数据结构+算法”
数据结构的分类
数据结构按照其逻辑结构可分为线性结构、树结构、图结构
线性结构:数据结构中的元素存在一对一的相互关系
树结构:数据结构中的元素存在一对多的相互关系
图结构:数据结构中的元素存在多对多的相互关系
栈
栈(Stack)是一个数据集合,可以理解为只能在一端进行插入或删除操作的列表。
栈的特点:后进先出 LIFO(last-in, first-out)
栈的概念:栈顶、栈底
栈的基本操作:
进栈(压栈):push
出栈:pop
取栈顶:gettop
栈的实现
使用一般的列表结构即可实现栈
进栈:li.append
出栈:li.pop
取栈顶:li[-1]
栈的应用-括号匹配问题
括号匹配问题:给一个字符串,其中包含小括号、中括号、大括号,求该字符串中的括号是否匹配。
例如:
()()[]{} 匹配
([{()}]) 匹配
[]( 不匹配
[(]) 不匹配
python代码实现
class Stack:
def __init__(self):
self.stack = []
def push(self, value):
# 压栈
self.stack.append(value)
def pop(self):
# 出栈
if not self.is_empty():
return self.stack.pop()
raise IndexError("stack is empty.")
def get_top(self):
# 取栈顶
if not self.is_empty():
return self.stack[-1]
raise IndexError("stack is empty.")
def is_empty(self):
# 是否为空栈
return len(self.stack) == 0
def bracket_match(s):
"""
括号匹配问题解决
:param s:
:return:
"""
stack = Stack()
bracket_dict = {
")": "(",
"]": "[",
"}": "{",
}
for val in s:
if val in "([{":
# 左括号 进栈
stack.push(val)
else:
# 右括号进行匹配
if stack.is_empty():
# 栈为空
return False
elif stack.get_top() == bracket_dict[val]:
# 括号匹配
stack.pop()
else:
# 括号不匹配
return False
return stack.is_empty()
print(bracket_match("(){}{{}}[]"))
print(bracket_match("{(}"))
队列
队列(Queue)是一个数据集合,仅允许在列表的一端进行插入,另一端进行删除。
通常,称进数据的一端为 "队尾"(rear),出数据的一端为 "队头"(front),数据元素进队列的过程称为 "入队",出队列的过程称为 "出队"。
队列的性质:先进先出(First-in, First-out)
队列不能用列表简单实现(列表的append和pop太慢)
双向队列
双向队列的两端都支持进队和出队操作
双向队列的基本操作:
-
队首进队
-
队首出队
-
队尾进队
-
队尾出队
环形队列实现
环形队列:当队尾指针front == Maxsize + 1时,再前进一个位置就自动到0.
队首指针前进1:front = (front + 1) % MaxSize
队尾指针前进1:rear = (rear + 1) % MaxSize
队空条件:rear == front
队满条件:(rear + 1) % MaxSize == front
python代码实现
"""
环形队列:当队尾指针front == Maxsize + 1时,再前进一个位置就自动到0.
队首指针前进1:front = (front + 1) % MaxSize
队尾指针前进1:rear = (rear + 1) % MaxSize
队空条件:rear == front
队满条件:(rear + 1) % MaxSize == front
"""
class Queue:
def __init__(self, size=100):
self.queue = [0 for _ in range(size)]
self.rear = 0
self.front = 0
self.size = size
def push(self, value):
if not self.is_filled():
self.rear = (self.rear + 1) % self.size
self.queue[self.rear] = value
else:
# 一、队列满就顶掉最前面的一个元素
self.front = (self.front + 1) % self.size
self.rear = (self.rear + 1) % self.size
self.queue[self.front] = value
# 二、队列满报错
# raise IndexError("queue is filled.")
def pop(self):
if not self.is_empty():
self.front = (self.front + 1) % self.size
return self.queue[self.front]
raise IndexError("queue is empty.")
def is_empty(self):
return self.rear == self.front
def is_filled(self):
return (self.rear + 1) % self.size == self.front
q = Queue(5)
print(q.is_empty())
for i in range(4):
q.push(i)
print(q.is_filled())
print(q.pop())
栈和队列的应用-迷宫问题
给出一个二维列表,表示迷宫(0表示通道,1表示围墙)。给出算法,求一条走出迷宫的路径。
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]
]
栈---深度优先搜索
应用栈解决迷宫问题,叫做深度优先搜索(一条路走到黑),也叫做回溯法。
思路:从一个节点开始,任意找下一个能走的点,当找不到能走的点时,退回上一个点寻找是否有其他方向的点。
使用栈存储当前路径,后进先出,方便回退到上一个点。可以规定按照上右下左的顺序去看能不能走下一个点。(先看上面一个点能不能走,不能走走右边一个点,再不能走走下面一个点,最后不能走就走左边一个点)
python代码实现
"""栈解决迷宫问题"""
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, y + 1), # 右
lambda x, y: (x + 1, y), # 下
lambda x, y: (x, y - 1), # 左
]
def maze_path(x1, y1, x2, y2):
"""
:param x1: 起点x坐标
:param y1: 起点y坐标
:param x2: 终点x坐标
:param y2: 终点y坐标
:return: bool
"""
stack = [] # 创建栈
stack.append((x1, y1)) # 将起点位置进栈
while len(stack) > 0: # 栈长度为0时为空栈,即没有路径
current_node = stack[-1] # 当前的节点
# 判断是否到终点了
if current_node[0] == x2 and current_node[1] == y2:
# 到终点就输出路径
for path in stack:
print(path)
print("到终点了.")
return True
# 循环找下一个节点
for d in dirs:
next_node = d(current_node[0], current_node[1]) # 找出下一个节点
if maze[next_node[0]][next_node[1]] == 0: # 等于0表示该路可以走
stack.append(next_node) # 找到下一个节点后保存一下节点
maze[next_node[0]][next_node[1]] = 2 # 设置成2表示这个点走过了
break # 找到下一个节点后退出循环
else:
# 没有找到下一个节点,回退
maze[next_node[0]][next_node[1]] = 2 # 设置成2表示这个点走过了
stack.pop()
else:
# 没有路径
print("没有路径可以到终点.")
return False
maze_path(1, 1, 8, 8)
总结算法就是:创建一个空栈,首先将起点位置进栈。当栈不空时循环;获取栈顶元素,寻找下一个可走的相邻方块,如果找不到可走的相邻方块,说明当前位置是死胡同,进行回溯(就是讲当前位置出栈,看前面的点是否还有别的出路)
使用栈来解决迷宫问题,它的路径并不是最短的,很可能会绕远,如果想走最短路径可以使用队列来做。
队列---广度优先搜索
应用队列解决迷宫问题,叫做广度优先搜索。
思路:从一个节点开始,寻找所有接下来能继续走的点,继续不断寻找,直到找到出口。(需要一个额外的列表记录哪个节点是由哪个节点过来的,从终点往前推导得出迷宫路径。)
使用队列存储当前正在考虑的节点,如图:
python代码实现
"""队列解决迷宫问题"""
from collections 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, y + 1), # 右
lambda x, y: (x + 1, y), # 下
lambda x, y: (x, y - 1), # 左
]
def print_path(path_list):
current_node = path_list[-1] # 拿到最后一个节点(因为要从后往前循环,所以拿最后一个)
real_path_list = [] # 最后正确的路径
while current_node[2] != -1: # 起点的[2]是-1,从后往前以此循环
real_path_list.append(current_node[0:2]) # 前两个是路径,所以是[0:2]也就是0,1
current_node = path_list[current_node[2]] # 重新赋值 要判断的条件
real_path_list.append(current_node[0:2]) # 把开始的起点放进去
real_path_list.reverse() # 因为是从后往前循环,所以是逆序的,要翻转回来
for path in real_path_list: # 循环输入
print(path)
def maze_path(x1, y1, x2, y2):
q = deque() # 创建队列
q.append((x1, y1, -1)) # 将起点入列(第三个参数表示该节点是由哪个节点走过来的,起点没有前一个节点就用-1表示)
path_list = [] # 保存走过的路径
while len(q) > 0: # 队列长度为0表示无路可走了
current_node = q.popleft() # 当前的节点
path_list.append(current_node) # 将当前节点加入到保存路径的列表中
# 判断是否到终点了
if current_node[0] == x2 and current_node[1] == y2:
print_path(path_list) # 打印路径
return True
# 循环遍历查找下一个节点
for d in dirs:
next_node = d(current_node[0], current_node[1]) # 找到下一个可走的节点
if maze[next_node[0]][next_node[1]] == 0: # 节点为0表示路可以走
q.append( # 将在一个可走的节点入队列,第三个表示是由哪个路径走过来的(下标)
(next_node[0], next_node[1], len(path_list) - 1)
)
maze[next_node[0]][next_node[1]] = 2 # 标记节点已经走过了
else:
# 无路可走
print("没有路径可以到终点.")
return False
maze_path(1, 1, 8, 8)
链表
单链表
链表是由一系列节点组成的元素集合。每个节点包含两部分,数据域item和指向下一个节点的指针next。通过节点之间的相互连接,最终串联成一个链表。
"""简单实现"""
class Node:
def __init__(self, val):
self.data = val
self.next = None
lk = Node(1)
lk1 = Node(2)
lk2 = Node(3)
lk.next = lk1
lk1.next = lk2
print(lk.data)
print(lk.next.data)
print(lk.next.next.data)
创建链表
-
头插法
-
尾插法
class Node:
def __init__(self, val):
self.data = val
self.next = None
def create_link_head(li):
# 从后往前链接:1<--2<--3<--4...
head = Node(li[0])
for element in li[1:]:
node = Node(element)
node.next = head
head = node
return head
def create_link_tail(li):
# 从前往后链接:1-->2-->3-->4...
head = Node(li[0])
tail = head
for element in li[1:]:
node = Node(element)
tail.next = node
tail = node
return head
def print_lk(lk):
while lk:
print(lk.data, end=',')
lk = lk.next
lk = create_link_head([1, 3, 4, 5, 6])
print_lk(lk)
print("\n---")
lk = create_link_tail([1, 3, 4, 5, 6])
print_lk(lk)
链表节点的插入
p.next = curNode.next
curNode.next = p
链表节点的删除
p = curNode.next
curNode.next = curNode.next.next
del p
双链表
双链表的每个节点有两个指针:一个指向后一个节点,另一个指向前一个节点。
"""双链表"""
class Node:
def __init__(self, val):
self.data = val
self.next = None
self.prev = None
lk = Node(1)
lk1 = Node(2)
lk2 = Node(3)
lk.next = lk1
lk1.next = lk2
lk1.prev = lk
lk2.prev = lk1
print(lk.data)
print(lk.next.data)
print(lk.next.next.data)
print(lk.prev)
print(lk.next.prev.data)
print(lk.next.next.prev.prev.data)
双链表节点的插入
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(n)
-
按下标查找:O(n)
-
在某元素后插入:顺序表是O(n),链表是O(1)
-
删除某元素:顺序表是O(n),链表是O(1)
链表与顺序表
链表在插入和删除的操作上明显快于顺序表
链表的内存可以更灵活的分配
试利用链表重新实现栈和队列
链表这种链式存储的数据结构对树和图的结构有很大的启发性
哈希表
哈希表=直接寻址表+哈希
直接寻址表
直接寻址表,定义了一个所有关键字的域集合U,根据这个U生成一个包含所有域值的列表T
- 直接寻址技术优点:
- 当数据量较小时,操作相对简单有效
- 直接寻址技术缺点:
- 当U很大时,建立列表T,所消耗的内存空间非常大
- 如果U非常大,而实际出现的key非常少,这样就对空间造成了极大的浪费
- 当关键字key不是数字的时候就无法处理了
hash(哈希)
-
基本概念:hash,也称作散列、杂凑或者哈希,是一种线性表的存储结构。哈希表由一个直接寻址表和一个哈希函数组成,哈希函数h()将元素的关键字k处理得到唯一的存储元素的位置信息。能把任意长度的输入,通过hash算法转化为固定长度的输出,是一种压缩映射。最大的特点就是从无法从结果推算出输入内容,所以称之为不可逆算法。本质上是通过哈希函数计算数据存储位置的数据结构。
-
python中的字典和集合就是利用哈希表实现的。
-
hash表是对直接寻址表的改进,具体的
- 构建大小为m的寻址表T,[0,1,2,3,……,m-1]
- 将关键字为k的元素,放置在T中的 h(k)所指定的位置上,这里不是直接用k作为键进行存储,而是使用h(k)
- h() 是哈希函数,将U中的key映射到寻址表T中
-
哈希特性:
- 过程不可逆,即拿到结果也不能反解出输入值是多少
- 计算速度极快
-
哈希表存在的问题:
- 哈希冲突
- 由于关键字的数量是无限的,而hash创建的寻址表T是有限的,所以必定会出现存储位置冲突或重复,即当h(k)和h(k1)的值相同时,两个值的存储位置是冲突的。
- 哈希冲突的解决方式:
- 方式一:开放寻址法,如果哈希函数返回的位置已经有值,则继续向后探寻新的位置来存储这个值。而继续向下探寻位置的方法有
- 线性探查,如果位置i被占用,则向下探查i+1,i+2 ……直至空位置
- 二次探查,如果位置i被占用,则依次探查i+12,i+22……直至空位置
- 二度哈希:有n个哈希函数,等使用第一个哈希函数h1,发生冲突时,依次尝试使用h2,h3……
- 方式二:拉链法,哈希表每个位置都链接一个链表,当冲突发生时,冲突的元素被加到该位置链表后面
- 方式一:开放寻址法,如果哈希函数返回的位置已经有值,则继续向后探寻新的位置来存储这个值。而继续向下探寻位置的方法有
- 常见的哈希函数:
- 除法哈希:
h(k)=k%m
- 乘法哈希:
h(k) = floor(m*(A*key%1))
- 全域哈希:
h(k) = ((a*key+b) mod p) mod m a,b =1,2,……p-1
- 除法哈希:
- hash表的基本操作:
- insert(key,value) 插入键值对
- get(key) 根据键取值
- delete(key) 删除键值对
- 主要的应用方向:
- python等语言的字典、集合等数据类型就是基于hash实现的
- 密码,很多密码都是基于hash构造出来的
- 可以用于检测文件的一致性(文件是否带病毒;网盘系统是否已经存在此文件,存在的话就秒传,将已经存在文件链接过来)
- 用于数字签名
MD5
,SHA-1
,SHA-2
-
hash表的简单实现(拉链法,单链表),python代码实现:
-
""" 使用链表,实现hash。构建一个类似于集合的hash表 """ class LinkList: """对链表的进一步封装""" class Node: """链表的节点类""" def __init__(self, item=None): self.item = item self.next = None class LinkListIterator: """将链表转为迭代器类""" def __init__(self, node): self.node = node def __next__(self): if self.node: current_node = self.node self.node = current_node.next return current_node.item else: raise StopIteration def __iter__(self): return self def __init__(self, iterable=None): self.head = None self.tail = None if iterable: self.extend(iterable) def append(self, obj): """使用尾插法,进行插入""" current_node = LinkList.Node(obj) if not self.head: self.head = current_node self.tail = current_node else: self.tail.next = current_node self.tail = current_node def extend(self, iterable): for obj in iterable: self.append(obj) def find(self, obj): for n in self: if n == obj: return True else: return False def __iter__(self): return self.LinkListIterator(self.head) def __repr__(self): return "<<" + ", ".join(map(str, self)) + ">>" class HashTable: """实现集合的部分功能,不可重复,可查询(拉链法)""" def __init__(self, size=100): self.size = size self.T = [LinkList() for i in range(self.size)] def h(self, k): return k % self.size def find(self, k): hash_num = self.h(k) # 获取给定数值的hash值 return self.T[hash_num].find(k) def insert(self, k): if self.find(k): # 不可重复 print("Duplicated Insert.") else: hash_num = self.h(k) # 获取插入的值的hash值 self.T[hash_num].append(k) ht = HashTable() ht.insert(0) ht.insert(1) ht.insert(3) ht.insert(103) ht.insert(200) print(",".join(map(str, ht.T))) print(ht.find(0)) print(ht.find(103)) print(ht.find(200))
-
Python中hash的使用,通过调用hashlib模块实现
- 通过hashlib调用不同的hash函数,实现不同数据的不同处理。
- 实例化hash对象,hash_obj = hash.hash函数类型() ,实例化过程中,可以通过添加参数,实现加盐操作
- 常见的hash函数类型有md5,sha1,sha224,sha256,sha384,sha512等
- hash_obj.update( 处理内容 ) 注意,需要进行处理的内容必须是bytes类型
- hash_obj.digest() ,返回当前已处理内容的hash值(内容摘要),返回值二进制数据
- hash_obj.hexdigest(),返回当前已处理内容的hash值(内容摘要),返回值是十六进制数据
- 通过hashlib调用不同的hash函数,实现不同数据的不同处理。
-
MD5
-
曾是密码学中常用的hash函数(现在用的越来越少,安全性稍微高的东西不会用它了)
- 主要特点:
- 同样的消息
MD5
相同 - 可以快速计算出任意长度的消息的
MD5
- 除非暴力撞库,否则基本不能从哈希值反推出消息本身(
md5
已经出来很多年了,很多数据库已经计算运行了很久) - 两条消息之间,即使是只有微笑差别,其
MD5
应该是完全不同,不相关的 - 在有限时间内,无法人工构建出
MD5
值相同的两个不同的信息
- 同样的消息
- 主要特点:
-
应用举例:
- 验证文件,例如是否是相同的文件,上传下载文件的完整性、云盘秒传等
-
-
SHA-1
,SHA-2
-
SHA-1
和MD5
都是曾经在密码学上经常用到的hash算法,但是目前,较为常用的是SHA-2
-
SHA-2
算法包含,SHA-224
,SHA-256
,SHA-384
,SHA-512
,SHA-512/224
,SHA-512/256
等算法,其中224,256,384与512代表的是哈希值的长度
-
树结构
树是一种可以递归定义的数据结构
-
基本概念
- 根节点,叶子节点
- 树的深度(高度),有根节点向下走的层数
- 树的度,每个节点上分叉的数量就是节点的度。所有节点中最大的度作为树的度
- 子节点、父节点:
- 子树
-
树的简单示例---模拟文件管理目录结构,Python代码示例:
-
"""树的简单示例---模拟文件管理目录结构""" class Node: def __init__(self, name, type="dir"): self.name = name self.type = type self.children = [] self.parent = None def __str__(self): return self.name 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 += "/" new_node = Node(name) self.now.children.append(new_node) new_node.parent = self.now def ls(self): print(self.now.children) def cd(self, name): # 必须以"/"结尾 if name[-1] != "/": name += "/" # cd到上一层 if name == "../": self.now = self.now.parent return # 以相对路径cd for child in self.now.children: if child.name == name and child.type == "dir": self.now = child return raise ValueError("invalid value.") tree = FileSystemTree() tree.mkdir("var/") tree.mkdir("bin/") tree.mkdir("usr/") tree.ls() tree.cd("bin/") tree.mkdir("python/") tree.ls() tree.cd("../") tree.ls()
-
二叉树
-
class BinaryTree: def __init__(self, data): self.data = data self.lchild = None self.rchild = None
-
度不超过二的树,
-
对于完全二叉树,可以很方便的通过列表的线性存储方式,此外,完全二叉树以及其他二叉树都可以通过链表的链式存储实现
-
链式存储形式搭建的二叉树在执行效率上相对较高
-
链式存储的二叉树的创建
-
可以用单链表(只向下记录子节点信息)也可以用双链表(存储了子节点和父节点的信息)
-
二叉树的遍历
-
分为前序遍历、中序遍历、后序遍历和层级遍历四种方式
- 前序遍历,根在最前,每层的遍历顺序为,根左右
- 中序遍历,根在结果中间,每层遍历顺序为,左根右
- 后序遍历,根在结果最后,每层遍历顺序为,左右根
- 层级遍历,自根开始,每层进行逐一遍历
- 结合两种遍历打印的结果,就能反推出二叉树的原始形态
-
遍历的简单实现,Python代码示例:
-
from collections import deque class BinaryTreeNode: def __init__(self, data): self.data = data self.lchild = None self.rchild = None a = BinaryTreeNode("A") b = BinaryTreeNode("B") c = BinaryTreeNode("C") d = BinaryTreeNode("D") e = BinaryTreeNode("E") f = BinaryTreeNode("F") g = BinaryTreeNode("G") e.lchild = a e.rchild = g a.rchild = c c.lchild = b c.rchild = d g.rchild = f root = e # 树的根 """ E A G C F B D """ def perv_order(root): """前序遍历""" if root: print(root.data, end=",") perv_order(root.lchild) perv_order(root.rchild) def in_order(root): """中序遍历""" if root: in_order(root.lchild) print(root.data, end=",") in_order(root.rchild) def post_order(root): """后序遍历""" if root: post_order(root.lchild) post_order(root.rchild) print(root.data, end=",") def level_order(root): """层次遍历:借助队列""" queue = deque() queue.append(root) while len(queue) > 0: now_node = queue.popleft() print(now_node.data, end=",") if now_node.lchild: queue.append(now_node.lchild) if now_node.rchild: queue.append(now_node.rchild) perv_order(root) print() in_order(root) print() post_order(root) print() level_order(root) print()
-
-
-
二叉搜索树
-
-
二叉搜索树是一个二叉树,并且满足下面性质,
-
x是二叉树的一个节点,如果
y1,y2
分别是左右子树的任一子节点,那么有y1.key <= x.key <= y2.key
-
二叉搜索树的基本操作 :
- 二叉搜索树基本操作包含遍历,插入,删除,查找等
- 遍历,分为前序、中序、后序、层级
- 插入
- 查找
- 删除操作
- 1.如果要删除的节点是叶子节点:直接删除
- 2如果要删除的节点只有一个孩子(左孩子或右孩子两种情况):将此节点的父亲与孩子连接,然后删除该节点。
- 3如果要删除的节点有两个孩子:将其右子树的最小节点(该节点最多有一个右孩子)删除,并替换当前节点。
-
"""二叉搜索树""" class BinaryTreeNode: def __init__(self, data): self.data = data self.lchild = None self.rchild = None self.parent = None class BST: def __init__(self, li=None): self.root = None if li: for val in li: self.insert_no_recursion(val) def insert(self, node, data): if not self.root: node = BinaryTreeNode(data) elif data < node.data: node.lchild = self.insert(node.lchild, data) node.lchild.parent = node elif data > node.data: node.rchild = self.insert(node.rchild, data) node.rchild.parent = node return node def insert_no_recursion(self, data): node = self.root if not node: # 空树 self.root = BinaryTreeNode(data) return while True: if data < node.data: if node.lchild: node = node.lchild else: node.lchild = BinaryTreeNode(data) node.lchild.parent = node return elif data > node.data: if node.rchild: node = node.rchild else: node.rchild = BinaryTreeNode(data) node.rchild.parent = node return else: return def query(self, node, data): if not node: return None if data < node.data: self.query(node.lchild, data) elif data > node.data: self.query(node.rchild, data) else: return node def query_no_recursion(self, data): node = self.root while node: if data < node.data: node = node.lchild elif data > node.data: node = node.rchild else: return node return None def __remove_node_1(self, node): # 情况1:如果要删除的节点是叶子节点:直接删除 if node == node.parent.lchild: # 要删除的是左节点 node.parent.lchild = None elif node == node.parent.rchild: # 要删除的是右节点 node.parent.rchild = None else: # 只有一个节点(根节点),直接删除根节点 self.root = None def __remove_node_2_1(self, node): # 情况2.1:如果要删除的节点只有一个孩子(左孩子):将此节点的父亲与孩子连接,然后删除该节点。 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_2_2(self, node): # 情况2.1:如果要删除的节点只有一个孩子(右孩子):将此节点的父亲与孩子连接,然后删除该节点。 if not node.parent: # 根节点 self.root = node.rchild node.rchild.parent = None # 可有可无 elif node == node.parent.lchild: # 左结点 node.parent.lchild = node.rchild node.rchild.parent = node.parent else: # 右节点 node.parent.rchild = node.rchild node.rchild.parent = node.parent def delete(self, data): if self.root: # 不是空树 node = self.query_no_recursion(data) if not node: # 节点不存在 return False # 情况3:如果要删除的节点有两个孩子:将其右子树的最小节点(该节点最多有一个右孩子)删除,并替换当前节点。 if not node.lchild and not node.rchild: # 只是一个叶子结点 self.__remove_node_1(node) elif not node.rchild: # 只有左孩子,没有右孩子 self.__remove_node_2_1(node) elif not node.lchild: # 只有右孩子,没有左孩子 self.__remove_node_2_2(node) else: # 左右孩子都有 tmp_node = node.rchild while tmp_node.lchild: tmp_node = tmp_node.lchild node.data = tmp_node.data # 删除 if tmp_node.rchild: self.__remove_node_2_2(tmp_node) else: self.__remove_node_1(tmp_node) else: return None def in_order(self, root): if root: self.in_order(root.lchild) print(root.data, end=",") self.in_order(root.rchild) tree = BST([1, 4, 2, 5, 3, 8, 6, 9, 7]) tree.in_order(tree.root) print("\n------") tree.delete(1) tree.delete(3) tree.delete(9) tree.in_order(tree.root)
-
二叉搜索树的效率
-
平均情况下,二叉搜索树进行搜索的时间复杂度为O(nlgn)。
-
最坏情况下,二叉搜索树可能非常偏斜。
-
解决方案:
-
随机化插入
-
AVL树
-
-
-
-
AVL树
-
一棵AVL树是其每个结点的平衡因子绝对值最多相差1的二叉搜索树。
-
什么是平衡因子?平衡因子就是二叉排序树中每个结点的左子树和右子树的高度差。看下图:
- 根结点45的平衡因子为-1 (左子树高度2,右子树高度3)
- 50结点的平衡因子为-2 (左子树高度0,右子树高度2)
- 40结点的平衡因子为0 (左子树高度1,右子树高度1)
-
当AVL树的平衡因为插入节点或者删除节点导致原来平衡打乱,可以通过旋转来进行修正
- (左左)右:当不平衡是对左子节点的左子树进行插入导致的,则通过右旋调整
- (右右)左:当不平衡是对右子节点上的右子树进行插入导致的,则通过左旋调整
- (左右)左右:对左子节点的右子树进行插入导致的不平衡,通过先左旋再右旋调整
- (右左)右左:对右子节点上的左子树插入导致的不平衡,通过先右旋再左旋调整
-
右旋:(原来的右孩子旋转后变为左孩子)
-
左旋:(原来的左孩子旋转后变为右孩子)
-
右旋-左旋
-
左旋-右旋
-
class AVLNode: def __init__(self, data): self.data = data self.lchild = None self.rchild = None self.parent = None self.bf = 0 # balance factor class AVL: def __init__(self, li=None): self.root = None if li: for val in li: self.insert_no_recursion(val) def in_order(self, root): if root: self.in_order(root.lchild) print(root.data, end=",") self.in_order(root.rchild) def perv_order(self, root): if root: print(root.data, end=",") self.perv_order(root.lchild) self.perv_order(root.rchild) def post_order(self, root): if root: self.post_order(root.lchild) self.post_order(root.rchild) print(root.data, end=",") def rotate_left(self, p, c): """ 左旋 :param p: 节点 :param c: 节点 :return: """ s2 = c.lchild p.rchild = s2 if s2: # s2不存在时,None没有parent属性 s2.parent = p c.lchild = p p.parent = c # 设置balance factor p.bf = 0 c.bf = 0 # 返回根节点 return c def rotate_right(self, p, c): # 右旋 s2 = c.rchild p.lchild = 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 g.lchild = p p.parent = g # bf的三种情况(左边沉标记为-1,右边沉标记为1,平衡是0) if g.bf < 0: # 左边沉 p.bf = 0 c.bf = 1 elif g.bf > 0: # 右边沉 p.bf = -1 c.bf = 0 else: # s1,s2,s3,s4都是空 p.bf = 0 c.bf = 0 g.bf = 0 return g def rotate_left_right(self, p, c): """先左旋再右旋""" # 旋转后的根节点 g = c.rchild # 旋转要动的节点(左旋) 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 g.rchild = p p.parent = g # bf的三种情况 if g.bf < 0: c.bf = 0 p.bf = 1 elif g.bf > 0: c.bf = -1 p.bf = 0 else: c.bf = 0 p.bf = 0 g.bf = 0 return g def insert_no_recursion(self, data): # 插入 node = self.root if not node: self.root = AVLNode(data) return while True: if data < node.data: if node.lchild: node = node.lchild else: node.lchild = AVLNode(data) node.lchild.parent = node insert_node = node.lchild # 当前插入的节点 break elif data > node.data: if node.rchild: node = node.rchild else: node.rchild = AVLNode(data) node.rchild.parent = node insert_node = node.rchild # 当前插入的节点 break else: # 不允许插入相同值 return # 更新bf while insert_node.parent: if insert_node.parent.lchild == insert_node: # 如果是从左边上来的,左边就更沉了 # 两种情况:左边的左孩子(右旋);左边的右孩子(左旋-右旋) # 更新insert_node.parent.bf-=1 if insert_node.parent.bf < 0: # 原来是-1,更新后变为-2 m = insert_node.parent.parent # 为了下面的链接 x = insert_node.parent # 旋转后的parent会改变,所以要记录一下 if insert_node.bf < 0: # 左边的左孩子 n = self.rotate_right(insert_node.parent, insert_node) else: # bf为0时,不会再往上传,所以不可能等于0,只有大于小于两种情况 # 左边的右孩子 n = self.rotate_left_right(insert_node.parent, insert_node) elif insert_node.parent.bf > 0: # 原来是1,更新后变为0 insert_node.parent.bf = 0 break # bf为0时,树的高度没有发生变化,所以不会影响原来的bf,直接退出循环 else: # 原来是0,更新后变为-1 insert_node.parent.bf = -1 insert_node = insert_node.parent continue # 满足绝对值不大于2,继续 else: # 如果是从右边上来的,右边就更沉了 # 两种情况:右边的左孩子(右旋-左旋);右边的右孩子(左旋) # 更新insert_node.parent.bf+=1 if insert_node.parent.bf < 0: # 原来是-1,更新后是0 insert_node.parent.bf = 0 break # bf为0时,树的高度没有发生变化,所以不会影响原来的bf,直接退出循环 elif insert_node.parent.bf > 0: # 原来是1,更新后变为2 m = insert_node.parent.parent # 为了下面的链接 x = insert_node.parent # 旋转后的parent会改变,所以要记录一下 if insert_node.bf < 0: n = self.rotate_right_left(insert_node.parent, insert_node) else: # bf为0时,不会再往上传,所以不可能等于0,只有大于小于两种情况 n = self.rotate_left(insert_node, insert_node) else: # 原来是0,更新后变为1 insert_node.parent.bf = 1 insert_node = insert_node.parent continue # 满足绝对值不大于2,继续 # 链接旋转后的子树 n.parent = m if m: if x == m.lchild: m.lchild = n else: m.rchild = n else: self.root = n break tree = AVL([9, 8, 7, 6, 5, 4, 3, 2, 1]) tree.perv_order(tree.root) print("") tree.in_order(tree.root)
-
-
B树,是对AVL树的扩展,是一个多叉(多路)自平衡的搜索树。常见的应用场景是数据库的索引。
-
B+树,是对B树的再次升级扩展