Python进阶指南7:排序算法和树
1.排序算法
1.1算法稳定性
所谓排序,使一串记录,按照其中的某个或某些关键字的大小,递增或递减的排列起来的操作
排序算法,就是如何使得记录按照要求排列的方法
排序算法在很多领域是非常重要
在大量数据的处理方面:一个优秀的算法可以节省大量的资源。
在各个领域中考虑到数据的各种限制和规范,要得到一个符合实际的优秀算法,得经过大量的推理和分析
举个例子:
把以上数据进行升序排列:
☆ 假定在待排序的记录序列中,存在多个具有相同的关键字的记录
☆ 若经过排序,这些记录的相对次序保持不变,则称这种排序算法是稳定的, 否则称为不稳定的
记忆:具有相同关键字的纪录经过排序后,相对位置保持不变,这样的算法是稳定性算法
再来看一个例子:
升序排序后的结果
无序数据具有2个关键字,
先按照关键字1排序,
若关键字1值相同,再按照关键字2排序。
稳定性算法具有良好的作用。
1.2排序算法
排序犹如一把将混乱变为秩序的魔法钥匙,使我们能以更高效的方式理解与处理数据。
无论是简单的升序,还是复杂的分类排列,排序都向我们展示了数据的和谐美感。
☆ 冒泡排序
冒泡思路
冒泡排序(bubble sort)通过连续地比较与交换相邻元素实现排序。这个过程就像气泡从底部升到顶部一样,因此得名冒泡排序。
可视化展示网站:https://visualgo.net/zh/sorting
冒泡过程可以利用元素交换操作来模拟:从数组最左端开始向右遍历,依次比较相邻元素大小,如果“左元素 > 右元素”就交换二者。遍历完成后,最大的元素会被移动到数组的最右端。
第一轮第一步排序:
第一轮第二步排序:
第一轮第三步排序:
第一轮第四步排序:
第一轮第五步排序:
第一轮最终排序结果:
重复以上步骤,直至完成最终排序。
冒泡步骤
设列表的长度为 n ,冒泡排序的步骤如上图所示。
- 首先,对 n 个元素执行“冒泡”,将列表的最大元素交换至正确位置。
- 接下来,对剩余 n−1 个元素执行“冒泡”,将第二大元素交换至正确位置。
- 以此类推,经过 n−1 轮“冒泡”后,前 n−1 大的元素都被交换至正确位置。
- 仅剩的一个元素必定是最小元素,无须排序,因此列表排序完成。
代码实现
# 冒泡排序
def bubble_sort(my_list):# 获取列表的元素个数list_length = len(my_list)"""以[5,3,4,7,2]注意:排完序以后,小的在前,大的在后。当前遍历到的元素与后一个元素进行对比和交互第一轮循环:i=0,list_length=5内层循环:j元素的索引初始值=0,j要循环到【索引为3】结束j=3,元素是7,后一个元素的索引=j+1结合上面的注意实现,得到内层循环要list_length-1第一轮循环的结果:[3,4,5,2,7]第二轮循环:i=1,list_length=5,列表中的最后的元素7不用再纳入下一次循环内层循环:j元素的索引初始值=0,j要循环到【索引为2】结束因此最终内层循环的range的表达式要写出list_length-1-1第二轮循环的结果:[3,4,2,5,7]第三轮循环:i=2,list_length=5,列表中的最后的元素5,7不用再纳入下一次循环内层循环:j元素的索引初始值=0,j要循环到【索引为1】结束因此最终内层循环的range的表达式要写出list_length-1-2综上所述:因此最终内层循环的range的表达式要写出list_length-1-i"""for i in range(list_length):# 外层循环控制循环的次数for j in range(list_length-1-i):# 内层循环控制每轮中各个元素值的大小对比if my_list[j]>my_list[j+1]:# 交互两个元素的值my_list[j],my_list[j+1] = my_list[j+1],my_list[j]if __name__ == '__main__':my_list = [5,3,4,7,2]bubble_sort(my_list)print(my_list)
效率优化
我们发现,如果某轮“冒泡”中没有执行任何交换操作,说明数组已经完成排序,可直接返回结果。因此,可以增加一个标志位 flag
来监测这种情况,一旦出现就立即返回。
经过优化,冒泡排序的最差时间复杂度和平均时间复杂度仍为 O(n2) ;但当输入数组完全有序时,可达到最佳时间复杂度 O(n)
# 冒泡排序
def bubble_sort(my_list):# 获取列表的元素个数list_length = len(my_list)"""以[5,3,4,7,2]注意:排完序以后,小的在前,大的在后。当前遍历到的元素与后一个元素进行对比和交互第一轮循环:i=0,list_length=5内层循环:j元素的索引初始值=0,j要循环到【索引为3】结束j=3,元素是7,后一个元素的索引=j+1结合上面的注意实现,得到内层循环要list_length-1第一轮循环的结果:[3,4,5,2,7]第二轮循环:i=1,list_length=5,列表中的最后的元素7不用再纳入下一次循环内层循环:j元素的索引初始值=0,j要循环到【索引为2】结束因此最终内层循环的range的表达式要写出list_length-1-1第二轮循环的结果:[3,4,2,5,7]第三轮循环:i=2,list_length=5,列表中的最后的元素5,7不用再纳入下一次循环内层循环:j元素的索引初始值=0,j要循环到【索引为1】结束因此最终内层循环的range的表达式要写出list_length-1-2综上所述:因此最终内层循环的range的表达式要写出list_length-1-i"""# 是否有元素交互。False表示没有,True表示有flag = Falsefor i in range(list_length-1):print(f"外层循环{i}")# 外层循环控制循环的次数for j in range(list_length-1-i):# 内层循环控制每轮中各个元素值的大小对比if my_list[j]>my_list[j+1]:# 交互两个元素的值my_list[j],my_list[j+1] = my_list[j+1],my_list[j]flag = True# 为什么能够直接在这里判断然后结束循环。核心原因是冒泡排序的第一轮循环会得到最大值# 如果第一轮循环的过程中,都没有发生交换。那么后续更加不可能有交换if not flag:breakif __name__ == '__main__':# my_list = [5,3,4,7,2]my_list = [1,2,3,4]bubble_sort(my_list)print(my_list)
☆ 选择排序
选择排序(selection sort)的工作原理非常简单:开启一个循环,每轮从未排序区间选择最小的元素,将其放到已排序区间的末尾。
排序思路
设列表的长度为 n ,选择排序的算法流程如下图所示:
第一轮:
第一轮:
第二轮:
第二轮:
第三轮:
第三轮:
第四轮:
第四轮:
第五轮:
第五轮:
完成排序,最终结果:
排序步骤
- 初始状态下,所有元素未排序,即未排序(索引)区间为 [0,n−1] 。
- 选取区间 [0,n−1] 中的最小元素,将其与索引 0 处的元素交换。完成后,数组前 1 个元素已排序。
- 选取区间 [1,n−1] 中的最小元素,将其与索引 1 处的元素交换。完成后,数组前 2 个元素已排序。
- 以此类推。经过 n−1 轮选择与交换后,数组前 n−1 个元素已排序。
- 仅剩的一个元素必定是最大元素,无须排序,因此数组排序完成。
代码实现
def select_sort(my_list):# 获取列表的元素个数list_length = len(my_list)for i in range(list_length-1):# 外层循环控制排序轮次"""以[5, 3, 4, 7, 2]为例。list_length固定值为5第一轮:i=0,临时的最小值初始化是5,那么需要拿3,4,7,2与该值进行大小比较因此,j的初始化索引=1,直到=list_length-1结束第一轮的结果:[2,3,4,7,5] 第二轮:i=1,临时的最小值初始化是3,那么需要拿4,7,5与该值进行大小比较因此,j的初始化索引=2,直到=list_length-1结束所以:内层循环的range中表达式range(i+1,list_length)"""# 临时的最小值的索引min_index = ifor j in range(i+1,list_length):# 内层循环用来在剩余的数据中找到最小值# 比较大小。如果当前的元素值比临时的最小值要小,更新最小值的索引if my_list[j]<my_list[min_index]:min_index = j# 经过上面的循环以后能够找到本轮的最小值索引,然后调整元素的索引if min_index!=i:my_list[min_index],my_list[i] = my_list[i],my_list[min_index]if __name__ == '__main__':my_list = [5, 3, 4, 7, 2]# my_list = [1, 2, 3, 4]select_sort(my_list)print(my_list)
2.树
2.1树的基本概念
树是一种一对多关系的数据结构,主要分为:
- 多叉树
- 每个结点有0、或者多个子节点
- 没有父节点的结点成为根节点
- 每一个非根节点有且只有一个父节点
- 除了根节点外,每个子节点可以分为多个互不相交的子树
- 二叉树
- 每个结点有0、1、2 个子节点
- 没有父节点的结点成为根节点
- 每一个非根节点有且只有一个父节点
- 除了根节点外,每个子节点可以分为多个互不相交的子树
2.2树的相关术语
2.3二叉树的种类
2.4二叉树的存储
顺序存储、链式存储。树在存储的时候,要存储什么?
- 值
- 结点关系
如果树是完全二叉树、满二叉树,可以使用顺序存储。大多数构建出的树都不是完全、满二叉树,所以使用链式存储比较多。
class TreeNode:def __init__(self):self.item = valueself.parent = 父亲self.lchild = 左边树self.rchild = 右边树
对于树而言,只要拿到根节点,就相当于拿到整棵树。
完全二叉树适合顺序结构存储,但其插入删除元素效率较差。
大多数的二叉树都是使用链式结构存储。
2.5树的应用场景_数据库索引
2.6二叉树的概念和性质
2.7广度优先遍历
class Node(object):"""节点类"""def __init__(self, item):self.item = itemself.lchild = Noneself.rchild = Noneclass BinaryTree(object):"""二叉树"""def __init__(self, node=None):self.root = nodedef add(self, item):"""添加节点"""passdef bradh_travel(self):"""广度优先遍历"""pass
- 深度优先遍历:沿着某一个路径遍历到叶子结点,再从另外一个路径遍历,直到遍历完所有的结点
- 广度优先遍历:按照层次进行遍历
2.8添加节点思路分析
- 初始操作:初始化队列、将根节点入队、创建新结点
- 重复执行:
- 获得并弹出队头元素
- 如果当前结点的左右子结点不为空,则将其左右子节点入队
- 如果当前结点的左右子节点为空,则将新结点挂到为空的左子结点、或者右子节点
- 获得并弹出队头元素
2.9遍历方法的实现
class Node(object):"""节点类"""def __init__(self, item):self.item = itemself.lchild = Noneself.rchild = Noneclass BinaryTree(object):"""完全二叉树"""def __init__(self, node=None):self.root = nodedef add(self, item):"""添加节点"""# 初始操作:初始化队列if self.root == None:self.root = Node(item)return# 队列queue = []# 根节点入队queue.append(self.root)while True:# 从头部取出数据node = queue.pop(0)# 判断左节点是否为空if node.lchild == None:node.lchild = Node(item)returnelse:queue.append(node.lchild)if node.rchild == None:node.rchild = Node(item)returnelse:queue.append(node.rchild)def breadh_travel(self):"""广度优先遍历"""if self.root == 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)
if __name__ == '__main__':tree = BinaryTree()tree.add("A")tree.add("B")tree.add("C")tree.add("D")tree.add("E")tree.add("F")tree.add("G")tree.add("H")tree.add("I")tree.breadh_travel()
2.10二叉树的三种深度优先遍历
先序遍历:先访问根节点、再访问左子树、最后访问右子树
中序遍历:先访问左子树、再访问根节点、最后访问右子树
后序遍历:先访问左子树、再访问右子树、最后访问根节点
- 无论那种遍历方式,都是先访问左子树、再访问右子树
- 碰到根节点就输出、碰到左子树、右子树就递归 注意:左子树右子树是一棵树所以递归;根节点是一个节点所以打印输出
2.11二叉树的三种深度优先遍历代码实现
class Node(object):"""节点类"""def __init__(self, item):self.item = itemself.lchild = Noneself.rchild = Noneclass BinaryTree(object):"""完全二叉树"""def __init__(self, node=None):self.root = nodedef add(self, item):"""添加节点"""if self.root == None:self.root = Node(item)return# 队列queue = []# 从尾部添加数据queue.append(self.root)while True:# 从头部取出数据node = queue.pop(0)# 判断左节点是否为空if node.lchild == None:node.lchild = Node(item)returnelse:queue.append(node.lchild)if node.rchild == None:node.rchild = Node(item)returnelse:queue.append(node.rchild)def breadh_travel(self):"""广度优先遍历"""if self.root == 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("0")tree.add("1")tree.add("2")tree.add("3")tree.add("4")tree.add("5")tree.add("6")tree.add("7")tree.add("8")tree.add("9")tree.preorder_travel(tree.root)print()tree.inorder_travel(tree.root)print()tree.postorder_travel(tree.root)
2.12二叉树由遍历结果反推二叉树的结构
我们需要知道先序遍历结果和中序遍历结果、或者后序遍历结果和中序遍历结果才能够确定唯一一棵树。
只知道先序遍历、后序遍历结果,不能保证确定唯一的一棵树。
通过先序遍历可以确定哪个元素是根节点,通过中序遍历可以知道左子树都有那些结点、右子树都有那些结点。