第 3 章 数据结构 进阶
3.1 线性表
线性表:按照某种线性关系存储下来的表
分类:
线性表 | 说明 |
---|---|
顺序表 | 将数据放在一个连续的存储空间 |
链表 | 把数据分散存储,按照某种关系连成串 分类:单向链表、双向链表、单向循环链表 |
3.2 顺序表
3.2.1顺序表形式
对象:顺序表里的数据
分类/表现形式:
基本布局:同类数据
元素外置:异类数据
原因:操作系统 自动分析传入的数据类型,分配存储空间
- 如果是同类数据,那么放在连续存储空间
- 如果是异类数据,那么放在合适的存储空间
基本布局:
同类型数据
数据的查看方法:
物理地址
逻辑地址 --- 使用的
起始位置 + 偏移量*(元素个数-1)
十六进制的表示形式 0x
元素外置:
异类数据 存储再不连续的空间
中间手段 ----逻辑地址
单独找一个连续的存储空间,来存储各个数据的逻辑地址
通过跳转的方式找到具体的元素
3.2.2 顺序表结构
顺序表结构:
A 基本属性
B的空间容量大小
B空间存储元素的个数
B 元素存储空间
结构存储样式:
结构存储样式 | 说明 |
---|---|
一体式 | 基本属性信息和元素存储空间 放在一个连续空间 |
分离式 | 基本属性信息和元素存储空间 分别存放 |
3.2.3 顺序表实践
A.内容更改
数据存储一旦定性,就永远不会再变,一旦变了,就是生成了一个新的对象空间
一体式
分离式
灵活 ******
B.容量更改
容量扩充策略
线性增加
倍数增长 ***********
3.2.4 顺序表常见操作
增加
头部 保序增加 O(n)
尾部 保序增加 O(1)
指定位置
方法1 非保序增加 O(1)
方法2 保序增加 O(n)
删除
头部 保序删除 O(n)
尾部 保序删除 O(1)
指定位置
方法1 非保序删除 O(1)
方法2 保序删除 O(n)
3.3 单向链表
3.3.1 单向链表简介
单向链表是什么
基本单位:结点
结点属性:元素数据区域item + 下一个元素的地址next
多个结点连成串
链表基本术语
结点
头结点
尾结点
前驱结点(前结点)
后继结点(后结点)
单向链表特点
1 找到头结点,所有信息都找得到
2 尾结点的next属性指向为None
3 保存链表,只需要保存头结点的地址就可以了
为什么用链表
灵活、空间利用率高
3.3.2 python 链表解析
3.3.3 实践 之 基本属性
结点的内置属性
class BaseNode(object):
# 结点的基本属性
def __init__(self,item):
self.item = item
self.next = None
3.3.4 实践 之 操作分析
单向链表的基本属性
class SingleLinkList(object):
# 单向链表的基本属性 -- 规则3(保存链表,只需要保存头结点的地址就可以了 )
def __init__(self,node=None):
self.__head = node
信息查看类 | 类方法 |
---|---|
链表是否为空 | is_empty() |
链表元素数量 | length() |
链表内容查看 | travel() |
链表内容搜索 | search(item) |
内容增加类 | 类方法 |
---|---|
插头增 | add(item) |
插尾增 | append(item) |
指定位置增 | insert(pos,item) |
内容删除类 | 类方法 |
---|---|
内容删除 | remove(item) |
3.3.5 实践 之 查看
判空
思路:规则3
# 判断链表是否为空
def is_empty(self):
# 思路:规则3,隐含规则1(头结点找不到就是空链表)
return self.__head is None
数量
# 判断链表元素个数
def length(self):
# 准备工作:标签、计数器
# 找到当前链表的头结点
cur = self.__head
count = 0
# 遍历循环:条件来源于规则2
# 查找尾结点
while cur is not None:
# 对计数进行加1
count += 1
# 移动当前的结点位置
cur = cur.next # 验证的是规则1
# 输出最终计数
return count
关键点:
写的时候,按非空链表来写,
分析的时候,要分析特殊情况,空链表
所有内容
# 输出链表所有内容
def travel(self):
# 准备工作:标签
cur = self.__head
# 遍历循环:条件来源于规则2
while cur is not None:
# 输出每个结点的内容,设置分隔符为空格(print默认会换行)
print(cur.item,end=" ")
# 移动到当前的结点位置
cur = cur.next
# 还原分割符为换行符
print("")
关键点:
循环遍历的退出条件:while cur is not None
结点内容输出:print(cur.item, end=" ")
结点移动:cur = cur.next
搜寻内容
# 查询我要的内容
def search(self, item):
# 找到当前链表的头信息
cur = self.__head
# 查找尾结点
while cur is not None:
# 如果当前结点的内容就是我们要查找的,就返回True
if cur.item == item:
return True
# 移动当前结点位置到下一节点
cur = cur.next
# 如果整个循环都找不到我们要的内容,就返回 False
return False
3.3.6 实践 之 增加
增加元素的流程:
新结点 – 找位置 – 改属性
头部
# 头部增加结点
def add(self, item):
# 1.实例化新结点
# 定义一个存储数据的新结点
node = BaseNode(item)
# 2.将新结点加入到链表中
# 指定新结点的next属性为之前链表的头信息
node.next = self.__head
# 3.修改链表的默认属性:规则3(保存链表,只保存头结点地址)
# 指定当前链表的头结点为新结点
self.__head = node
尾部
# 尾部元素增加
def append(self,item):
# 1.实例化新结点
# 定义一个存储数据的新结点
node = BaseNode(item)
# 2.找位置,将新结点加入到链表中
# 2.1 空位置
# 如果当前链表为空,那么直接指定头信息为新结点即可
if self.is_empty():
self.__head = node
# 2.2 非空位置
# 如果当前链表不为空
else:
# 找到当前链表的头信息,然后找到尾结点
cur = self.__head
while cur.next is not None:
cur = cur.next
# 退出循环,cur就处于尾结点位置
# 找到尾结点就退出循环,尾结点的next指向新结点即可
cur.next = node
关键点:
思路:
新结点
找位置
空链表:改self.__head属性
非空链表:找尾结点;
改为结点的next属性
指定位置
# 指定位置增加
def insert(self, pos, item):
# 流程:新结点---找位置---判断位置---改属性
# 1.新增加结点
# 定义一个存储数据的新结点
node = BaseNode(item)
# 2.找位置
# 2.1 头、尾
# 头部添加内容
if pos <= 0:
self.add(item)
# 在尾部添加元素
elif pos >= self.length():
self.append(item)
# 2.2 中间位置
# 在中间添加元素
else:
# 准备工作:标签、计数器
# 找到头信息,并且开始设置计数器初始值
cur = self.__head
count = 0
# 找到要插入位置的上一个位置:
while count <= (pos - 1):
count += 1
cur = cur.next
# 退出循环,找到指定位置的前一个位置,改属性
# 设置新结点的 next 属性为当前结点的下一个结点
node.next = cur.next
# 设置当前结点的 next 属性为新结点
cur.next = node
关键点:
思路
新增结点
找位置
头
尾
中间位置
找pos-1位置
改属性(2个属性)
3.3.7 实践 之 删除
流程:
判断
链表
是否为空 – 判断内容
是否匹配 – 判断位置
在哪里 – 改属性
# 指定元素删除
def remove(self, item):
# 判空 --- 内容匹配 --- 位置 --- 改属性 --- 异常(内容不匹配)
# 准备工作: 两标签 cur pre
# 找到当前链表的头信息
cur = self.__head
# 自定义一个当前结点的上一个结点 pre,默认值是 None(头结点)
pre = None
# 1.链表不为空
while cur is not None:
# 2.内容匹配
# 如果当前结点的item就是要删除的内容
if cur.item == item:
# 2.1头部位置
# 如果当前结点 A 就是当前链表的头信息
if cur == self.__head:
# 设置当前链表的头信息为当前结点 A的下一个结点
self.__head = cur.next
# 2.2 其他位置
# 如果当前结点不是头结点
else:
# 将当前结点 B 的上一结点 A 的next属性设定为下一个结点 C (pre --- cur --- next)
pre.next = cur.next
# 确认移除后,退出函数即可
return
# 3.内容不匹配
else:
# 如果不是要找的删除元素,移动 cur 的标签,继续进行判断
pre = cur
# 将当前结点 A 的next属性为下一个结点 C
cur = cur.next
思路
准备工作:两个标签pre cur
链表不为空
内容匹配
头部位置 --- 改变头部信息
其他位置 --- pre.next = cur.next
内容不匹配
异常情况 --- 标签对下一个结点处理
3.3.8 链表 VS 顺序表
单向链表特性:头找尾,尾的next为None,保存单向链表只需保存头结点
单向链表增加思路:示例化结点-找位置-改属性
单向链表删除思路:判空-内容匹配-找位置-改属性-处异常
3.4 双向链表
3.4.1 双向链表简介
定义:
基本属性:结点
结点属性:
前一个结点的地址:pre
数据区域item
后一个结点的地址:next
特点:
尾结点的next属性指向为None
头结点的pre属性指向为None
从左向后的方向上来说:双向链表就是单向链表
3.4.2 实践 之 基本属性
3.4.3 实践 之 操作分析
3.4.4 实践 之 增加
头部
# 头部增加
def add(self, item):
# 1.新增结点
# 定义一个存储数据的新结点
node = BaseNode(item)
# 2.找位置(省略)
# 3.改属性
# 3.1 新结点属性修改
# 指定新结点的next属性为之前链表的头信息
node.next = self.__head
# 指定当前链表的的头信息为 新结点
self.__head = node
# 3.2 老的头结点属性修改
# 如果链表不为空
if node.next:
# 修改老结点的pre属性为新结点
node.next.pre = node
尾部
# 尾部元素增加
def append(self, item):
# 1.实例化新结点
# 定义一个存储数据的新结点
node = BaseNode(item)
# 2.找位置,将新结点加入到链表中
# 2.1 空位置
# 如果当前链表为空,那么直接指定头信息为新结点即可
if self.is_empty():
self.__head = node
# 2.2 非空位置
# 如果当前链表不为空
else:
# 找到当前链表的头信息,然后找到尾结点
cur = self.__head
while cur.next is not None:
cur = cur.next
# 退出循环,cur就处于尾结点位置
# 指定新结点的 pre 属性为当前结点
node.pre = cur
# 找到尾结点就退出循环,尾结点
指定位置
# 指定位置增加
def insert(self, pos, item):
# 流程:新结点---找位置---判断位置---改属性
# 1.新增加结点
# 定义一个存储数据的新结点
node = BaseNode(item)
# 2.找位置
# 2.1 头、尾
# 头部添加内容
if pos <= 0:
self.add(item)
# 在尾部添加元素
elif pos >= self.length():
self.append(item)
# 2.2 中间位置
# 在中间添加元素
else:
# 准备工作:标签、计数器
# 找到头信息,并且开始设置计数器初始值
cur = self.__head
count = 0
# 找到要插入位置的上一个位置:
while count < pos:
count += 1
cur = cur.next
# 修改属性(cur.pre --- node --- cur --- cur.next)
# 设置新结点的pre属性为当前结点之前的上一个结点
node.pre = cur.pre
# 设置当前结点的上一个结点的next属性为新结点
cur.pre.next = node
# 设置新结点的next属性为当前结点
node.next = cur
# 设置当前结点的上一个结点的 next属性为新结点
cur.pre = node
思路:
新增结点
找位置
头
尾
中间位置
改属性
⚠️编写思路和单向链表一致
区别:
修改属性的时候,千万要注意顺序,不要影响后序操作
3.4.5 实践 之 删除
# 删除内容
def remove(self, item):
# 流程:判空 --- 内容匹配 --- 找位置 --- 改属性 --- 处理异常
# 准备工作
# 找到当前链表的头信息
cur = self.__head
# 1.链表不为空
# 遍历所有的结点
while cur is not None:
# 2 内容匹配
# 如果当前的item就是要删除的内容
if cur.item == item:
# 2.1 头部位置
# 如果当前结点 A 就是链表的头结点
if cur == self.__head:
# 设置当前链表的头信息为当前结点 A 的下一个结点 B
self.__head = cur.next
# 链表不是只有一个结点,新的头结点的pre属性指向None
if cur.next:
self.__head.pre = None
# 2.2 尾部位置
# 如果当前结点不是头结点(尾结点/中国南京爱你位置)
else:
# 将当前结点B的上一个结点的next属性设定为下一个结点C
cur.pre.next = cur.next
# 2.3 中间位置
# 如果当前结点不是尾结点
if cur.next:
cur.next.pre = cur.pre
# 删除完毕,就退出
return
# 3.内容不匹配
else:
# 将当前结点A的next属性为下一站点C
cur = cur.next
⚠️注意:
编写思路和单向链表一致
需要额外考虑pre属性
3.5 单向循环链表
3.5.1 单向循环链表简介
定义:
单向循环链表是一个特殊的单向链表
特点:
尾结点的next属性指向当前链表的头结点,self.__head
3.5.2 实践 之 基本属性
3.5.3 实践 之 操作分析
3.5.4 实践 之 查看
# 获取链表的数量
def length(self):
# 1.链表为空
if self.is_empty():
return 0
# 准备工作:标签cur 和 计数器count
cur = self.__head
# 原因:尾结点
count = 1
# 2.链表不为空
# 查找尾结点
while cur.next is not self.__head:
# 对计数进行递增
count += 1
# 移动当前的结点位置
cur = cur.next
# 退出循环的时候,cur标签在尾结点上,没有执行循环体内部语句
# 输出最终计数
return count
思考:
1 遍历的退出条件不一样了
2 空链表需要单独去处理
3 退出遍历循环的时候,并没有对尾结点进行处理
需要单独对尾结点进行处理
打印内容
# 获取链表的内容
def travel(self):
# 1.链表为空
if self.is_empty():
print("")
return
# 准备工作
cur = self.__head
# 2.链表不为空
while cur.next is not self.__head:
# 输出每个结点的内容,设置分隔符为空格
print(cur.item, end=" ")
# 移动当前的结点位置
cur = cur.next
# 退出循环的时候,没有对尾结点打印信息,需要单独处理尾结点
print(cur.item)
获取指定内容
# 获取链表的指定内容
def search(self, item):
# 1.链表为空
if self.is_empty():
return False
# 准备工作
cur = self.__head
# 2.链表不为空
while cur.next is not self.__head:
# 匹配内容
if cur.item == item:
return True
cur = cur.next
# 退出循环的时候,没有对尾结点打印信息,需要单独处理尾结点
if cur.item == item:
return True
else:
return False
3.5.5 实践 之 增加
头部
# 头部增加
def add(self, item):
# 1.实例化结点
# 定义一个存储数据的新结点
node = BaseNode(item)
# 2.空链表处理
# 对于空链表,我们需要单独处理
if self.is_empty():
self.__head = node
node.next = self.__head
# 3.非空链表处理
# 对于空链表,我们需要先定位头结点
cur = self.__head
# 查找尾结点,退出循环,表示 cur 处在尾结点
while cur.next is not self.__head:
cur = cur.next
# 4.尾结点处理
# 指定当前尾结点的next属性为新结点
cur.next = node
# 指定新结点的next属性为之前表的头结点
node.next = self.__head
# 指定的当前链表的头结点为新结点
self.__head = node
⚠️注意:
1.流程:新增结点 --- 位置 --- 先找尾结点 --- 改属性
2.单独对空链表处理
3.单独对尾结点处理
尾部增加
# 尾部增加
def append(self, item):
# 1.实例化结点
# 定义一个存储数据的新结点
node = BaseNode(item)
# 2.空链表处理
# 对于空链表,我们需要单独处理
if self.is_empty():
self.__head = node
node.next = self.__head
# 3.非空链表处理
# 对于空链表,我们需要先定位头结点
cur = self.__head
# 找尾结点
while cur.next is not self.__head:
cur = cur.next
# 4.尾结点处理
# 找到尾结点就退出循环,尾结点的next指向新结点即可
cur.next = node
# 将新结点的 next 指向当前链表的头结点
node.next = self.__head
指定位置增加
参考单向链表
# 指定位置增加
def insert(self, pos, item):
# 流程:新结点---找位置---判断位置---改属性
# 1.新增加结点
# 定义一个存储数据的新结点
node = BaseNode(item)
# 2.找位置
# 2.1 头、尾
# 头部添加内容
if pos <= 0:
self.add(item)
# 在尾部添加元素
elif pos >= self.length():
self.append(item)
# 2.2 中间位置
# 在中间添加元素
else:
# 准备工作:标签、计数器
# 找到头信息,并且开始设置计数器初始值
cur = self.__head
count = 0
# 找到要插入位置的上一个位置:
while count <= (pos - 1):
count += 1
cur = cur.next
# 退出循环,找到指定位置的前一个位置,改属性
# 设置新结点的 next 属性为当前结点的下一个结点
node.next = cur.next
# 设置当前结点的 next 属性为新结点
cur.next = node
3.5.6 实践 之 删除
# 指定位置删除
def remove(self, item):
# 1.空链表处理
# 如果链表为空
if self.is_empty():
return
# 准备工作:
# 找到当前链表的头信息
cur = self.__head
pre = None
# 2.非空链表处理
# 链表不为空的情况下
while cur.next is not self.__head:
# 3.内容匹配
# 如果当前结点 cur 的 item 就是要删除的内容
if cur.item == item:
# (1)头部位置
# 如果当前结点 cur 就是当前链表的头结点
if cur == self.__head:
# 在修改属性之前,先找尾结点 tnode(尾结点标识)
tnode = self.__head
while tnode.next is not self.__head:
tnode = tnode.next
# 属性修改
# 当前结点的下一个结点变为头结点
self.__head = cur.next
# 尾结点的下一个结点指向新的头结点
tnode.next = self.__head
# (2)中间位置
else:
# 将当前结点 cur 的上一结点的 next 属性设定为 cur 的下一个结点 C
pre.next = cur.next
return
# 4.内容不匹配
# 如果不是要找的删除元素,将当前结点 cur 的上一结点作为当前结点
pre = cur
# 将当前结点 cur 的 next 属性为下一结点 C
cur = cur.next
# 5. 尾结点处理
# 退出循环时候,cur已是尾结点
if cur.item == item:
# (1)链表只有一个元素
# 当cur是尾结点又是头结点,那么当前链表就是一个元素
if cur == self.__head:
self.__head = None
# (2)链表不止一个元素,
# cur 不是唯一的结点
else:
pre.next = self.__head
思想:
1.空链表处理
2.非空链表处理
内容匹配
头部位置
先找尾结点tnode
改属性
中间位置
改属性
内容不匹配
---移动pre 和 cur
3.尾结点处理
单一结点 的单向循环链表 ---空
多结点 的单向循环链表
---前一个结点的next属性为 self.__head()
3.6 栈
3.6.1 栈的简介
栈:数据结构
特点:
后进先出
原因:我们只能在栈顶的位置对数据进行插入和删除
3.6.2 栈的实践
栈的创建 | |
---|---|
创建一个新的空栈 | Stack() |
栈的信息查看 | |
---|---|
返回栈顶元素 | peek() |
返回栈的元素个数 | size() |
判断栈是否为空 | is_empty() |
栈的基本操作 | |
---|---|
添加一个新的元素item到栈顶 | push(item) |
弹出栈顶元素 | pop(item) |
class Stack(object):
"""栈的基本特性"""
def __init__(self):
self.__items = []
def is_empty(self):
"""判断栈是否为空"""
return self.__items == []
def peek(self):
"""查看栈顶元素"""
if self.is_empty():
return False
return self.__items[-1]
def size(self):
"""返回栈的长度"""
return len(self.__items)
def push(self, item):
"""添加元素到栈"""
self.__items.append(item)
def pop(self):
"""出栈动作"""
self.__items.pop()
3.7 队列
3.7.1 队列简介
队列:数据结构
特点:
先进先出
场景:消息队列
3.7.2 队列的实践
队列的创建 | |
---|---|
创建一个空的队列 | Queue() |
队列信息查看 | |
---|---|
判断一个队列是否为空 | is_empty() |
返回队列的大小 | size() |
队列的基本操作 | |
---|---|
往队列中添加一个item元素 | enqueue(item) |
从队列头部删除一个元素 | dequeue() |
class Queue(object):
"""队列的基本属性"""
def __init__(self):
self.__items = []
def is_empty(self):
"""判断队列是否为空"""
return self.__items == []
def size(self):
"""队列的长度获取"""
return len(self.__items)
def enqueue(self, item):
"""给队列添加元素"""
self.__items.append(item)
def dequeue(self):
"""将队列中的元素弹出"""
return self.__items.pop(0)
3.7.3 双端队列(了解)
双端队列(Deque,double-ended queue):
是一种具有
队列和栈
的性质的数据结构。
双端队列的创建 | |
---|---|
创建一个空的双端队列 | Deque() |
双端队列信息查看 | |
---|---|
判断双端队列是否为空 | is_empty() |
返回队列的大小 | size() |
双端队列的基本操作 | |
---|---|
从队头加入一个item元素 | add_front(item) |
从队尾加入一个item元素 | add_back(item) |
从队头删除一个item元素 | remove_front() |
从队尾删除一个item元素 | remove_back() |
class Deque(object):
"""双端队列"""
def __init__(self):
self.__items = []
def is_empty(self):
"""判断队列是否为空"""
return self.__items == []
def size(self):
"""返回队列大小"""
return len(self.items)
def add_font(self, item):
"""在对头添加元素"""
self.__items.insert(0, item)
def add_back(self, item):
"""在对尾添加元素"""
self.__items.append(item)
def remove_font(self):
"""从对头删除元素"""
return self.__items.pop(0)
def remove_back(self):
"""从对尾删除元素"""
return self.__items.pop()
3.8 树
3.8.1 树简介
基本概念:
树是一种数据结构,基本单位是节点。它是由n个节点组成一个具有层次关系的集合。
特点:队列中的数据不仅仅有前后关系,还有层次关系,
基本特点:
- 每个节点有0个或者多个
子节点
- 没有父节点的节点称为
根节点
- 每一个非根节点有且只有一个
父节点
- 除了根节点外,每个字节点可以分为多个不相交的
子树
基本术语
- 父节点:含有子结点的节点
- 子结点:有父节点的节点
- 兄弟结点:相同父节点的节点
- 叶结点或终端结点:没有子结点的节点
节点的祖先:根节点到该之间所有父节点的集合 - 节点的
度
:一个节点含有子节点的个数
- 节点的
层次
:根节点到该结点所经历的节点的个数 - 树的度:一棵树中,最大的节点的度
- 树的
高度/深度
:书中节点的最大层次,从根结点开始 - 森林:由m(m>=0)棵互不相交的树的
结合
树的种类
-
有序树 ——子节点之间有顺序关系
-
二叉树:每个节点最多含有两个子树(
度<2
)的树完全二叉树: 除了深度d层外的所有层满节点。满二叉树:所有层满节点
平衡二叉树(AVL):当且仅当任何节点的两棵子树的高度差不大于1的二叉树;
排序二叉树(二叉查找树(英语:Binary Search Tree),也称二叉搜索树、有序二叉树);
-
霍夫曼树(用于
信息编码
):带**权路径最短
**的二叉树称为哈夫曼树或最优二叉树; -
B树:一种对**
读写操作进行优化的自平衡的二叉查找树
**,能够保持数据有序,拥有多余两个子树。
-
-
无序树 ——子节点之间没有顺序关系
树的存储
数据存储结构 | |
---|---|
顺序存储 | (1)顺序存储常见的形式是列表 (2)将二叉树的所有数据,从 上到下分层 ,每层从左到右 ,依次编号,然后按照编号顺序把所有元素存储到一个存储空间中。(3)特点: 按照 编号顺序 存储,所以查询比较快;元素之间 只有顺序 ,没有关系;所有元素存储到 一个空间 ,所以整体占用空间大(4) 父节点和子节点的关系 父节点i找子节点: 左子节点位置: 2i+1 右子节点位置:2i+2 子节点i找父节点: 左子找父: (i-1)/2 右子找父:(i-2)/2 |
链式存储 | (1)顺序表只是保存元素的前后关系,无前有的调用关系,而二叉树的重点就是关系 ,所以二叉树通常以链式存储 (2)特点: 链式格式: 存储数据 + 关系索引 存储二叉树无非就是多几个关系索引而已 |
树的应用场景:
1.xml,html等
,那么编写这些东西的解析器的时候,不可避免用到树
2.路由协议
就是使用了树的算法
3.mysql数据库索引
4.文件系统
的目录结构
5.所以很多经典的AI算法
其实都是树搜索,此外机器学习中的decision tree
也是树结构
3.8.2 二叉树简介
二叉树是每个节点最多有
两个子树
的树结构。通常子树被称作“左子树”(left subtree)和“右子树”(right subtree)
二叉树的特性:
性质1: 在二叉树的第i层上至多有2^(i-1)个结点(i>0)
性质2: 深度为k的二叉树至多有2^k - 1个结点(k>0)
性质3: 对于任意一棵二叉树,如果其叶结点数为N0,而度 数为2的结点总数为N2,则N0=N2+1;
**性质4:**具有n个结点的完全二叉树的深度必为 log2(n+1)
**性质5:**对完全二叉树,若从上至下、从左至右编号,则编号为i 的结点,其左孩子编号必为2i,其右孩子编号必为2i+1;其双亲的编号必为i/2(i=1 时为根,除外)
对完全二叉树,若从上至下、从左至右编号:
-
起始根节点的编号为0
父节点i找子节点:
左子节点位置:2i+1
右子节点位置:2i+2
子节点i找父节点:
左子找父:(i-1)/2
右子找父:(i-2)/2
见二叉树
常见二叉树 | |
---|---|
完美 二叉树 | 除了 叶子结点 外 的每个结点都有两个孩子,每一层都被完全填充 |
完全 二叉树 | 除了 最后一层之外 的其他每一层都被完全填充,并且所有节点都保持向左对齐 |
完满 二叉树 | 所有 非叶子节点 的度都是2,除了叶子节点之外的所有节点都有两个子节点 |
3.8.3 二叉树实践
二叉树创建
节点 + 树的结构
节点的基本信息
- item: 真正存储的数据
- lsub: 左子节点的索引
- rsub: 右子节点的索引
二叉树的结构
根节点 + 添加节点
A.根节点
二叉树必定有一个根节点,而且根节点可以为空(表示树不存在)===构建二叉树必须指明树的基本属性:根节点
class BaseNode(object):
"""定义节点的基本属性"""
def __init__(self, item):
# 节点存储的内容
self.item = item
# 左侧子节点的索引值
self.lsub = None
# 右侧子节点的索引值
self.rsub = None
B.添加结点
(1)没有任何数据的二叉树
(2)有数据的二叉树
添加顺序:父节点—左子节点—右子结点
class Tree(object):
"""定义树的基本属性:根节点"""
# 流程: 根节点 --- 新节点 --- 根节点为空判断 --- 待处理父节点 --- 左右侧子结点为空判断
def __init__(self, node=None):
# 不能写成self.__root,因为肯定需要看
self.root = node
# 添加结点
def add(self, elem):
# 新节点
# 定义要添加到树结构中的节点信息
node = BaseNode(elem)
# 1.如果根结点不存在数据
# 如果树是空,则对根节点赋值,对应的特点:从上到下
if self.root == None:
self.root = node
# 如果根节点存在数据
else:
# 2.待处理父节点队列
# 使用一个临时列表来存储我们要处理的元素: 对应的特点-从左到右(待处理父节点队列)
queue = []
# 先把根节点放到我们要处理的临时队列中
queue.append(self.root)
# 给父节点添加数据
# 只要处理队列中有要处理的数据,那就要一直处理下去
while queue:
# 3.确定要操作的父节点
# 从队列的头部获取要处理的元素
cur = queue.pop(0)
# 4.左侧子结点是否有数据
# 判断要处理的左侧子节点为空
if cur.lsub == None:
# 把接受到的item放到左侧节点位置
cur.lsub = node
# 添加完毕后,退出
return
# 左侧子节点不为空,有数据
# 左侧节点有数据,把该数据追加到待处理父节点队列末尾
else:
queue.append(cur.lsub)
# 5.右侧子结点是否为空
if cur.rsub == None:
# 把接受到的item放到右侧节点位置
cur.rsub = node
# 添加完毕后,退出
return
# 右侧子节点不为空,有数据
# 右侧节点有数据,把该数据追加到待处理父节点队列末尾
else:
queue.append(cur.rsub)
思路:
添加新结点
空树
根节点赋值
非空树
待处理父节点处理
左右侧子结点处理
空:增加数据
非空:数据追加到待处理队列