for i in self.items:
print(i)
# 栈的大小
def size(self):
return len(self.items)
# 栈是否为空
def is\_empty(self):
return self.items == []
# return len(self.items) == 0
# 返回栈顶元素
def peek(self):
if self.is_empty():
return "栈空"
return self.items[self.size()-1]
# return self.items[-1]
### 1.5 堆
Python 中 `heapq` 模块是 **小顶堆**
实现 **大顶堆** 方法: 小顶堆的插入和弹出操作均将元素 **取反** 即可
from heapq import *
from random import shuffle
data = list(range(10))
shuffle(data)
print(f’原始数据为:{data}‘)
small_heap = []
for num in data:
heappush(small_heap, num)
print(f’创建的小顶堆为:{small_heap}’)
heap = []
for num in data:
heappush(heap, -num)
print(heap)
big_heap = [-heappop(heap) for _ in range(len(heap))]
print(f’输出的大顶堆为:{big_heap}')
总体思路:负负得正
[参考](https://bbs.csdn.net/topics/618545628)
### 1.6 二叉树
**1. 树**
树是一种数据结构,它是由 n 个有限结点组成的一个具有层次关系的集合。
树的基本性质如下:
![在这里插入图片描述](https://img-blog.csdnimg.cn/f41e51f8c29c469bac4013452b8eda6c.png)
**2. 二叉树的基本结构**
二叉树则是每个结点最多有两个子树的树结构,通常子树被称作“左子树”和“右子树”。
二叉树的一般性质:
* 二叉树是有序的(左右子树不能颠倒)
* 二叉树的第 k 层上的结点数目最多为
2
k
−
1
2^{k-1}
2k−1
* 深度为 h 的二叉树最多有
2
h
−
1
2^h-1
2h−1 个结点
* 设非空二叉树中度为0、1 和 2 的结点个数分别为
n
0
n\_0
n0 、
n
1
n\_1
n1 和
n
2
n\_2
n2,则
n
0
=
n
2
+
1
n\_0 = n\_2+1
n0=n2+1(叶子结点比二分支结点多一个)
>
> **注意:这里的层数 k ,深度 h 均是从 1 开始的**
>
>
>
其他常见的二叉树:
![在这里插入图片描述](https://img-blog.csdnimg.cn/222b1bea030443cbb8f578a08339a1fd.png)
>
> 注意:
>
>
> * 如果按层序从0开始编号,结点 i 的左孩子为:2i+1,结点 i 的右孩子为:2i+2,结点 i 的父结点为:(i-1)//2
> * 如果结点按层序从0开始编号,假设共有 n 个结点,若 i <= n//2-1 ,该结点为非终端结点,若 i > n//2-1 ,该结点为终端结点
>
>
>
![在这里插入图片描述](https://img-blog.csdnimg.cn/d417a57a3d354c13951a9adc9bbd176c.png)
>
> 二叉树通常以链式存储
>
>
>
**3. 二叉树的实现与基本操作**
定义结点类
class Node(object):
def __init__(self, item):
self.item = item
self.lchild = None
self.rchild = None
定义二叉树
class BinaryTree(object):
def __init__(self, node=None):
self.root = node
"""
思路分析:首先在队列中插入根结点,取出该结点,再判断该结点的左右子树是否为空,
左子结点不空,将其入队,右子结点不空,将其入队,
再分别判断左右结点的左右子结点是否为空,
循环往复,直到发现某个子结点为空,即把新结点添加进来
“”"
# 添加结点
def add(self, item):
node = Node(item)
# 二叉树为空
if self.root is None:
self.root = node
return
# 二叉树不空
queue = []
queue.append(self.root)
# 编译环境会提示,也可以直接写成:queue = [self.root]
while True:
# 从队头取出数据
node1 = queue.pop(0)
# 判断左结点是否为空
if node1.lchild is None:
node1.lchild = node
return
else:
queue.append(node1.lchild)
# 判断右结点是否为空
if node1.rchild is None:
node1.rchild = node
return
else:
queue.append(node1.rchild)
# 广度优先遍历,也叫层次遍历
def breadth(self):
if self.root is None:
return
queue = []
queue.append(self.root)
while len(queue) > 0:
# 取出数据
node = queue.pop(0)
print(node.item, end=" ")
# 判断左右子结点是否为空
if node.lchild is not None:
queue.append(node.lchild)
if node.rchild is not None:
queue.append(node.rchild)
# 深度优先遍历
# 先序遍历(根左右)
def preorder\_travel(self, root):
if root is not None:
print(root.item, end=" ")
self.preorder_travel(root.lchild)
self.preorder_travel(root.rchild)
# 中序遍历(左根右)
def inorder\_travel(self, root):
if root is not None:
self.inorder_travel(root.lchild)
print(root.item, end=" ")
self.inorder_travel(root.rchild)
# 后序遍历(左右根)
def postorder\_travel(self, root):
if root is not None:
self.postorder_travel(root.lchild)
self.postorder_travel(root.rchild)
print(root.item, end=" ")
if name == “__main__”:
tree = BinaryTree()
tree.add(1)
tree.add(2)
tree.add(3)
tree.add(4)
# 添加结点的代码逻辑就是将添加第一个结点设置为根结点
print(tree.root)
print()
# 层序遍历
tree.breadth() # 1 2 3 4
print()
# 前序遍历(根左右)
tree.preorder_travel(tree.root) # 1 2 4 3
print()
# # 中序遍历(左根右)
tree.inorder_travel(tree.root) # 4 2 1 3
print()
# # 后序遍历(左右根)
tree.postorder_travel(tree.root) # 4 2 3 1
>
> 注意:
>
>
> * 广度优先遍历基于队列
> * 深度优先遍历基于栈
>
>
>
**试试 LeetCode 相关题目吧**
* [102. 二叉树的层序遍历](https://bbs.csdn.net/topics/618545628)
* [107. 二叉树的层序遍历 II](https://bbs.csdn.net/topics/618545628)
* [144.二叉树的前序遍历](https://bbs.csdn.net/topics/618545628)
* [94. 二叉树的中序遍历](https://bbs.csdn.net/topics/618545628)
* [145. 二叉树的后序遍历](https://bbs.csdn.net/topics/618545628)
* [589. N 叉树的前序遍历](https://bbs.csdn.net/topics/618545628)
* [590. N 叉树的后序遍历](https://bbs.csdn.net/topics/618545628)
* [429. N 叉树的层序遍历](https://bbs.csdn.net/topics/618545628)
>
> 注意:二叉树遍历相关的题目在LeetCode环境中,省略了添加结点的代码逻辑,以及定义二叉树类中的初始化,要注意区分、根据具体题目应变。另外,其输入输出都是列表的形式,要注意
>
>
>
**4. 由遍历结果反推二叉树结构**
![在这里插入图片描述](https://img-blog.csdnimg.cn/6ace3ef1371f4a7684f145b85f4ca91d.png)
## 2 排序算法
**算法的稳定性**:假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变则称这种排序算法是稳定的,否则称为不稳定的
* 不稳定的排序算法:选择排序、快速排序、希尔排序、堆排序
* 稳定的排序算法:冒泡排序、插入排序、归并排序、基数排序
### 2.1 冒泡排序
对要进行排序的数据中相邻的数据进行两两比较,将较大的数据放在后面,依次对所有的数据进行操作,直至所有数据按要求完成排序
如果有n个数据进行排序,总共需要比较 n-1 次
每一次比较完毕,下一次的比较就会少一个数据参与
![在这里插入图片描述](https://img-blog.csdnimg.cn/1d7883ac5e544ee7b51a0228e65f194a.png)
def bubble_sort(lis):
n = len(lis)
# 控制比较的轮数
for j in range(n - 1):
count = 0
# 控制每一轮的比较次数
# -1是为了让数组不要越界
# -j是每一轮结束之后, 我们就会少比一个数字
for i in range(n - 1 - j):
if lis[i] > lis[i + 1]:
lis[i], lis[i + 1] = lis[i + 1], lis[i]
count += 1
# 算法优化
# 如果遍历一遍发现没有数字交换,退出循环,说明数列是有序的
if count == 0:
break
if name == “__main__”:
lis = [2, 7, 3, 6, 9, 4]
bubble_sort(lis)
print(lis)
>
> 总结:
>
>
> * 冒泡排序是稳定的
> * 最坏时间复杂度为
>
>
>
>
> O
>
>
> (
>
>
>
> n
>
>
> 2
>
>
>
> )
>
>
>
> O(n^2)
>
>
> O(n2)
> * 最优时间复杂度为
>
>
>
>
> O
>
>
> (
>
>
> n
>
>
> )
>
>
>
> O(n)
>
>
> O(n),遍历一遍发现没有任何元素发生了位置交换终止排序
>
>
>
### 2.2 快速排序
快速排序算法中,每一次递归时以第一个数为基准数 ,找到数组中所有比基准数小的。再找到所有比基准数大的。小的全部放左边,大的全部放右边,确定基准数的正确位置。
def quick_sort(lis, left, right):
# 递归的结束条件:left > right
if left > right:
return
# 存储临时变量,left0始终为0,right0始终为len(lis)-1
left0 = left
right0 = right
# 基准值
base = lis[left0]
# left != right
while left != right:
# 从右边开始找寻小于base的值
while lis[right] >= base and left < right:
right -= 1
# 从左边开始找寻大于base的值
while lis[left] <= base and left < right:
left += 1
# 交换两个数的值
lis[left], lis[right] = lis[right], lis[left]
# left=right
# 基准数归位
lis[left0], lis[left] = lis[left], lis[left0]
# 递归操作
quick_sort(lis, left0, left - 1)
quick_sort(lis, left + 1, right0) # quick\_sort(lis, left + 1, right0)
if name == ‘__main__’:
lis = [1, 2, 100, 50, 1000, 0, 10, 1]
quick_sort(lis, 0, len(lis) - 1)
print(lis)
>
> 总结:
>
>
> * 快速排序算法不稳定
> * 最好的时间复杂度:
>
>
>
>
> O
>
>
> (
>
>
> n
>
>
> l
>
>
> o
>
>
>
> g
>
>
> 2
>
>
>
> n
>
>
> )
>
>
>
> O(nlog\_2n)
>
>
> O(nlog2n),初始序列大小均匀,每一次选择的基准值将待排序的序列划分为均匀的两部分,递归深度最小,算法效率最高
> * 最坏的时间复杂度:
>
>
>
>
> O
>
>
> (
>
>
>
> n
>
>
> 2
>
>
>
> )
>
>
>
> O(n^2)
>
>
> O(n2),初始序列有序或逆序,每次选择的基准值都是靠边的元素,递归深度最大,算法效率最低
>
>
>
### 2.3 (简单)选择排序
第一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,然后再从剩余的未排序元素中寻找到最小(大)元素,然后放到已排序的序列的末尾,以此类推,直到全部待排序的数据元素的个数为零。
![在这里插入图片描述](https://img-blog.csdnimg.cn/53611e8afe094dedbb39f0dc9afb3852.png)
def select_sort(lis):
n = len(lis)
# 控制比较的轮数
for j in range(n - 1):
# 假定最小值的下标
min_index = j
# 控制每一轮的比较次数
for i in range(j + 1, n):
# 进行比较获得最小值下标
if lis[min_index] > lis[i]:
min_index = i
# 如果假定的最小值下标发生了变化,那么就进行交换
if min_index != j:
lis[min_index], lis[j] = lis[j], lis[min_index]
if name == “__main__”:
lis = [2, 7, 3, 6, 9, 4]
select_sort(lis)
print(lis)
>
> 总结:
>
>
> * 选择排序是不稳定的
> * 最坏时间复杂度为O(n^2)
> * 最优时间复杂度为O(n^2)
>
>
>
### 2.4 堆排序
堆排序是指利用堆(必须是一种完全二叉树)这种数据结构所设计的一种排序算法
其核心思想是:
* 建立(大或小)根堆:
+ 从最后一个非终端结点开始,把所有的非终端结点都检查一遍,是否满足(大或小)根堆的要求,如不满足,则与更(大或小)的结点进行交换,元素互换可能会破坏下一层的堆,需要采用相同的方法进行调整,得到(大或小)根堆。
* 排序:
+ 每一趟排序将堆顶元素加入有序子序列(堆顶元素与待排序序列中最后一个元素交换),并将待排序元素序列再次调整为(大或小)根堆
堆排序有以下两种:
* 大根堆:每个结点的值都大于等于其左右孩子结点的值,满足`arr[i] >= arr[2i+1] && arr[i] >= arr[2i+2]`,基于**大根堆的堆排序得到递增序列**
* 小根堆:每个结点的值都小于等于其左右孩子结点的值,满足`arr[i] <= arr[2i+1] && arr[i] <= arr[2i+2]`,基于**小根堆的堆排序得到递减序列**
>
> 注意:
>
>
> * 如果结点按层序从0开始编号,结点 i 的左孩子为:2i+1,结点 i 的右孩子为:2i+2,结点 i 的父结点为:(i-1)/2
> * 如果结点按层序从0开始编号,假设共有 n 个结点,若 i <= n/2-1 ,该结点为非终端结点,若 i > n/2-1 ,该结点为终端结点
>
>
>
以大根堆为例,小根堆同理
def adjust(arr, parent, length):
# parent:父结点的索引,length:参与调整的数组长度(结点个数)
# 左孩子的索引
child = parent * 2 + 1
while child < length:
# 如果右孩子存在,且右孩子大于左孩子
if child + 1 < length and arr[child + 1] > arr[child]:
child += 1 # child变成右孩子的索引
# 父结点的值小于左、右孩子,交换
if arr[parent] < arr[child]:
arr[parent], arr[child] = arr[child], arr[parent]
parent = child
# 此时,temp和child索引都指向了子结点与父结点交换后,原来的父结点应该插入的位置(原来的子结点的位置)
# 但是我们不确定这是不是这个结点的最终位置,也就是不确定原来的父元素(小元素)往下调整时会不会破坏下面的大根堆结构
child = parent \* 2 + 1 # 原来的子结点变成了父结点,再找它的左孩子,如果存在左孩子继续循环,调整根堆
else: # 父结点的值大于等于左、右孩子,不交换
break
def sort(arr):
# 建堆
# 从最后一个非终端结点【索引len(arr) // 2 - 1】开始向前遍历到根结点【索引0】
for i in range(len(arr) // 2 - 1, -1, -1):
adjust(arr, i, len(arr))
# 每一趟将堆顶元素加入有序子序列(堆顶元素与待排序列中的最后一个元素交换)
# i:待排序列中最后一个元素的索引
# 最后一个元素分别是从最后一个结点【索引len(arr) - 1】开始向前遍历到的第二个结点【索引1】
for i in range(len(arr) - 1, 0, -1):
# 堆顶和最后一个元素互换位置
arr[0], arr[i] = arr[i], arr[0]
# 从顶开始重新调整堆
adjust(arr, 0, i)
return arr
if name == “__main__”:
arr = [53, 17, 78, 9, 45, 65, 87, 32]
print(sort(arr))
>
> 总结:
>
>
> * 堆排序是不稳定的
> * 建堆的时间复杂度为:O(n),排序的时间复杂度为:O(nlogn),总的时间复杂度为:O(nlogn)
>
>
>
### 2.5 (直接)插入排序
插入排序的基本操作就是将一个数据插入到已经排好序的有序数据中,从而得到一个新的、个数加一的有序数据,算法适用于少量数据的排序
插入算法把要排序的数组分成两部分:
* 第一部分是有序的数字(这里可以默认数组第一个数字为有序的第一部分)
* 第二部分为无序的数字(这里除了第一个数字以外剩余的数字可以认为是无序的第二部分)
def insert_sort(lis):
n = len(lis)
# 控制比较的轮数,即无序数据的个数,一个数肯定是有序的,不用比较
for j in range(1, n):
# 控制每一轮的比较次数
# i取值范围[j,j-1,j-2,j-3,1]
# 取出无序部分的首个,在有序部分从后向前比较,插入到合适的位置
for i in range(j, 0, -1):
# 找到合适的位置安放无序数据
if lis[i] < lis[i - 1]:
lis[i], lis[i - 1] = lis[i - 1], lis[i]
else:
break
if name == “__main__”:
lis = [2, 7, 3, 6, 9, 4]
insert_sort(lis)
print(lis)
>
> 总结:
>
>
> * 直接插入排序是稳定的
> * 最坏时间复杂度为O(n^2),本身倒序
> * 最优时间复杂度为O(n),本身有序,每一轮只需比较一次
>
>
>
### 2.6 归并排序
def merge_sort(nums):
if len(nums) <= 1:
return nums
mid = len(nums) // 2
left = merge_sort(nums[:mid])
right = merge_sort(nums[mid:])
return merge(left, right)
def merge(left, right):
merged = []
i = j = 0
while i < len(left) and j < len(right):
if left[i] <= right[j]:
merged.append(left[i])
i += 1
else:
merged.append(right[j])
j += 1
while i < len(left):
merged.append(left[i])
i += 1
while j < len(right):
merged.append(right[j])
j += 1
return merged
>
> 总结:
>
>
> * 归并排序是稳定的
> * 假设数组长度为 n,则在归并排序的过程中,需要进行 logn 次划分,每次划分需要 O(n) 的合并操作,因此,归并排序的总体时间复杂度为
>
>
>
>
> O
>
>
> (
>
>
> n
>
>
> l
>
>
> o
>
>
> g
>
>
![img](https://img-blog.csdnimg.cn/img_convert/08b1e5024f99d52d33faf445dbca17bd.png)
![img](https://img-blog.csdnimg.cn/img_convert/6928906b3e46b840233f578f1126e0cc.png)
**网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。**
**[需要这份系统化资料的朋友,可以戳这里获取](https://bbs.csdn.net/topics/618545628)**
**一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!**
le i < len(left) and j < len(right):
if left[i] <= right[j]:
merged.append(left[i])
i += 1
else:
merged.append(right[j])
j += 1
while i < len(left):
merged.append(left[i])
i += 1
while j < len(right):
merged.append(right[j])
j += 1
return merged
总结:
- 归并排序是稳定的
- 假设数组长度为 n,则在归并排序的过程中,需要进行 logn 次划分,每次划分需要 O(n) 的合并操作,因此,归并排序的总体时间复杂度为
O
(
n
l
o
g
[外链图片转存中…(img-aDasTJ4Q-1714707939095)]
[外链图片转存中…(img-3Sf1tQlu-1714707939095)]
网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!