计算机算法与数据结构(基于python版)
关于为什么写这个,因为我不想花钱买纸质本了,也不想记纸质笔记了,电子版会方便很多,第二是我总忘记我学的知识,所以我写个markdowm文档的电子笔记,以供我和其他小伙伴进行参考与学习,本人撰写笔记仅用于学习交流,无任何商业用途。若笔记内容涉及侵权,请联系我删除。如需引用本笔记内容,请同时注明原始课程出处。若笔记中有理解错误或表述不当之处,责任在我,与原作者无关。
本笔记为个人在学习过程中的整理与总结。笔记中引用的课程PPT、图表、核心观点及数据,其知识产权归原作者/原讲者/原机构(陈斌老师)所有。本内容还有一些演示截图来自于博主皮蛋编程
前置知识
大o表示法:大O表示法是一种用于描述算法时间复杂度或空间复杂度的数学表示法。它用来衡量算法在最坏情况下,随着输入数据规模增大,算法运行时间(或内存占用)的增长趋势。简单来说就是用这个方法来表示时间复杂度和空间复杂度
- 什么是时间复杂度?
时间复杂度是衡量算法运行时间随输入数据规模增长而变化的快慢程度
常见的时间复杂度:
| 时间复杂度 | 名称 | 典型场景 | 代码示例 (n = 输入规模) |
|---|---|---|---|
| O(1) | 常数阶 | 数组随机访问、哈希表查找 | return arr[0] |
| O(log n) | 对数阶 | 二分查找、平衡二叉树操作 | while (low <= high) { mid = (low+high)/2; ... } |
| O(n) | 线性阶 | 遍历数组/链表、简单查找 | for i in range(n): print(i) |
| O(n log n) | 线性对数阶 | 快速排序、归并排序、堆排序 | mergesort(arr) 或 heapsort(arr) |
| O(n^2) | 平方阶 | 冒泡排序、选择排序、双层循环 | for i in range(n): for j in range(n): ... |
| O(n^3) | 立方阶 | 矩阵乘法(朴素算法)、三层循环 | for i in range(n): for j in range(n): for k in range(n): ... |
| O(2^n) | 指数阶 | 递归求解斐波那契(无优化)、子集枚举 | if n <= 1: return n; else: return f(n-1)+f(n-2) |
| O(n!) | 阶乘阶 | 旅行商问题暴力破解、全排列枚举 | permutations(arr) 的所有排列 |
这里可以先不深究,知道有这么个概念就好,时间复杂度可以用来衡量一个程序是否是《好程序》的方法
空间复杂度: 空间复杂度是衡量算法在运行过程中临时占用内存大小随输入数据规模 nn 增长的变化趋势。与时间复杂度类似,它也用大O表示法描述,关注的是内存消耗的增长速度
| 空间复杂度 | 名称 | 典型场景 | 示例 |
|---|---|---|---|
| (O(1)) | 常数空间 | 原地操作,只使用固定几个变量 | 冒泡排序、简单计算 |
| (O(\log n)) | 对数空间 | 递归栈深度(分治算法) | 二分查找(递归版) |
| (O(n)) | 线性空间 | 创建与输入等大的辅助数组 | 归并排序、动态规划 |
| (O(n^2)) | 平方空间 | 创建二维矩阵 | 图的邻接矩阵、动态规划(二维DP) |
我个人感觉空间复杂度在未来的讲解中较为少的出现,相比之下可以更注重一下时间复杂度
常见的算法时间复杂度
| 算法 | 最好情况 | 最坏情况 | 平均情况 | 空间复杂度 | 稳定性 |
|---|---|---|---|---|---|
| 归并排序 | O(n log n) | O(n log n) | O(n log n) | O(n) | 稳定 |
| 快速排序 | O(n log n) | O(n²) | O(n log n) | O(log n) | 不稳定 |
| 插入排序 | O(n) | O(n²) | O(n²) | O(1) | 稳定 |
| 冒泡排序 | O(n) | O(n²) | O(n²) | O(1) | 稳定 |
| 选择排序 | O(n²) | O(n²) | O(n²) | O(1) | 不稳定 |
| 堆排序 | O(n log n) | O(n log n) | O(n log n) | O(1) | 不稳定 |
| 希尔排序 | O(n) | O(n²) | O(n log n) | O(1) | 不稳定 |
| 计数排序 | O(n + k) | O(n + k) | O(n + k) | O(k) | 稳定 |
| 桶排序 | O(n + k) | O(n²) | O(n + k) | O(n + k) | 稳定 |
| 基数排序 | O(n × k) | O(n × k) | O(n × k) | O(n + k) | 稳定 |
数据结构
线性结构
首先提到数据结构,我们最先想到的就是线性结构,那么什么是线性结构呢,线性结构是数据结构中的一个基本概念,指的是一组数据元素之间存在一对一的线性关系。简单理解:就像一条线把数据串起来,每个元素最多有一个直接前驱(前面的元素)和一个直接后继(后面的元素)。
核心特征:
- 有且只有一个第一个元素(称为“首元素”),它没有前驱
- 有且只有一个最后一个元素(称为“尾元素”),它没有后继
- 除首尾外,每个元素都有唯一的前驱和唯一的后继
举个例子:比如现实中的火车车厢,购物排队,或者一摞书,在这里我们常见的线性结构有数组,链表,栈,队列
栈的抽象数据类型
栈是一种受限的线性数据结构,遵循后进先出原则。可以理解为叠盘子,只能在盘子顶部进行插入和删除操作。

在计算机这个大知识体系中,栈是最常用的数据结构之一,甚至可以说是计算机运行的基石,比如函数的调用栈,中断/异常处理栈等
栈的基本操作如下
| 操作 | 功能描述 |
|---|---|
Stack() | 创建一个空栈,不包含任何数据项 |
push(item) | 将item加入栈顶,无返回值 |
pop() | 将栈顶数据项移除,并返回,栈被修改 |
peek() | “窥视” 栈顶数据项,返回栈顶的数据项但不移除,栈不被修改 |
isEmpty() | 返回栈是否为空栈 |
size() | 返回栈中有多少个数据项 |
💡 温馨提示:Python 列表实现栈的效率选择
我们可以在 list 列表中,将任意一端定义为栈顶(index=0 或 index=-1),另一端自然成为栈底。
- 如果把 列表头部(
index=0) 作为栈顶:- 入栈需要用
insert(0, item),出栈用pop(0) - 每次操作都要移动列表里的所有元素,时间复杂度为 O(n),效率低,不推荐。
- 入栈需要用
- 如果把 列表尾部(
index=-1) 作为栈顶:- 入栈直接用
append(item),出栈直接用pop() - 操作仅针对列表末尾元素,时间复杂度为 O(1),效率最高,是推荐的实现方式。
- 入栈直接用
用python实现抽象数据类型Stack
#定义栈的类
class Stack:
def __init__(self):#初始化,创建一个空列表来存储栈元素
self.items = []
def isEmpty(self): #判断栈是否为空
return self.items == []
def push(self, item):#将元素压入栈顶
self.items.append(item)
def pop(self): #弹出栈顶元素
return self.items.pop()
def peek(self):#检查栈顶元素
return self.items[-1]
def size(self): #返回栈的大小
return len(self.items)
栈的基础应用举例
def Parenthesis_Judgment(Parent):
s = Stack()
Parenthesis_Result = True
for i in range(len(Parent)):
if Parent[i] == "(":
s.push(Parent[i])
elif Parent[i] == ")":
if s.isEmpty() == False:
s.pop()
else:
Parenthesis_Result = False
if s.isEmpty() == False:
Parenthesis_Result = False
return Parenthesis_Result
print(Parenthesis_Judgment("(())()"))
# 简单的括号匹配
思路说明:实例化栈对象,初始化Parenthesis_Result变量为True 然后对输入的括号进行遍历,如果是左括号就进行压栈,如果是右括号就看看栈内是否还有的左括号,如果有则出栈,如果没有则不匹配,右括号多出来了,然后遍历完后,看栈是否为空,如果不为空则多左括号
def divideBy2(decNumber,n):
remStack = Stack()
while decNumber > 0 :
rem = decNumber % n
remStack.push(rem)
decNumber = decNumber // n
binString = ""
while remStack.isEmpty() == False:
binString = binString + str(remStack.pop())
return binString
print(divideBy2(42,2))
print(divideBy2(42,16))
# 十进制转换二进制
思路说明:实例化栈对象,然后进行循环,如果要进行进制转换的数小于0,则一直去除以要除以的数,然后把余数入栈,除完之后把入栈的数据再出栈就是倒序的,然后就可以输出了
def infixToPostfix(s):
prec = {}
prec['*'] = 3
prec['/'] = 3
prec['+'] = 2
prec['-'] = 2
prec['('] = 1
# 设置优先级
stack = Stack()
postFixList = []
# 创建结束列表
tokenList = s.split()
# 把输入的内容切割成字符串,并且以上内容为初始化信息
for i in tokenList:
if i in "ABCDEFGHIJKLMNOPQSTUVWXYZ" or i in "0123456789" or i in "abcdefghijklmnopqstuvwxyz":
postFixList.append(i)
# 诺为字母或者数字则直接放入输出列表里
elif i == '(':
stack.push(i)
# 诺为(号则直接入栈然后等待)号
elif i == ')':
topToken = stack.pop()
while topToken != '(':
postFixList.append(topToken)
topToken = stack.pop()
# 诺为)号则一直出栈,直到遇到(号
else:
while (stack.isEmpty() != True) and (prec[stack.peek()] >= prec[i]):
# 如果栈不为空,并且当前操作符的优先级小于的时候把弹出的操作符加入到结束列表
postFixList.append(stack.pop())
stack.push(i)
#把大于等于当前操作符的操作符加入到结束队列后,在把当前操作符加入栈顶,循环到下一位再次执行一编
while stack.isEmpty() != True:
# 等到tokenList循环结束后,把栈里面的全部操作符放入结束队列中
postFixList.append(stack.pop())
return " ".join(postFixList)
# 输出结束队列
print(infixToPostfix("1 + 2 * 3 / 4"))
print(infixToPostfix("2 + 5 * 3 / 4 + 5 * 2"))
print(infixToPostfix("a * ( b + c * d ) + e"))
#中缀表达撒
思路说明:先创建优先级字典,然后初始值栈这个对象,并且创建结束列表方便后续打印输出,之后我们把输入的内容切割成字符串,然后开始遍历切割后的字符串,如果是大小写字母或者数字,则直接放入输出列表,如果是( 则直接入栈然后等待)的出栈,如果是)则把栈里的所有内容全部出栈,直到遇到 ),如果是其他的符号,则进行判断,如果栈不为空,,并且当前操作符小于等于栈顶的符号,则弹出操作符加入到结束队列,如果栈里操作符大于当前操作符则直接把当前操作符加入栈顶,等到遍历完tokenlist后,把栈的全部操作符放入结束队列,然后输出 栈在这里是一个“运算符候车室”——进来的运算符先在栈里排队,但优先级高的可以插队到前面(后进先出),括号则是强行让括号内的运算符先出去。
队列的抽象数据结构
队列(Queue) 是一种先进先出(FIFO, First-In-First-Out) 的线性数据结构。就像现实生活中的排队一样,先来的人先服务,后来的人排在队尾。

在计算机体系中,队列也十分重要,比如消息队列,缓冲区队列,调度队列
队列的基本操作如下
| 操作 | 功能描述 |
|---|---|
Queue() | 创建一个空队列对象,返回值为Queue对象 |
enqueue(item) | 将数据项item添加到队尾,无返回值 |
dequeue() | 从队首移除数据项,返回值为队首数据项,队列被修改 |
isEmpty() | 测试是否为空队列,返回值为布尔值 |
size() | 返回队列中数据项的个数 |
用python实现抽象数据类型queue
class Quque:
#初始化空队列
def __init__(self):
self.items = []
# 判断队列是否为空
def isEmpty(self):
return self.items == []
# 判断队列内的个数
def size(self):
return len(self.items)
# 压入队列尾部
def enqueue(self, item):
return self.items.insert(0,item)
#弹出队列头
def dequeue(self):
return self.items.pop()
队列的基础应用举例
def hotPotato(namelist,num):
simqueue = Quque()
# 初始化队列
for name in namelist:
# 遍历列表,把所以内容入队列
simqueue.enqueue(name)
while simqueue.size() > 1:
for i in range(num):
simqueue.enqueue(simqueue.dequeue())
#队列出队列一次,在入队列一次
print(simqueue.items)
simqueue.dequeue()
#传递七次后,将队列移出
return simqueue.dequeue()
# 把最后一个幸存者说出来
print(hotPotato(["BILIBILI","CAIXUKUN","WUYIFAN","WUYANZU","DICK","JIKECHEN","WOSHISHEI"],7))
#约瑟夫问题
思路说明:先初始化队列,然后遍历列表把数组放入队列中,每次移动一个人,就会自动切换为下一个,队首的人先出队,再马上回到队尾。这样持续多少次,然后抽到谁,谁就出队列,再次循环,直到遇到最后一人
import random
class Printer:
def __init__(self, ppm):
self.pagerate = ppm # 打印速度
self.currentTask = None # 打印任务
self.timeRemaining = 0 # 任务倒计时
def tick(self):
if self.currentTask != None:
self.timeRemaining = self.timeRemaining - 1
if self.timeRemaining <= 0:
self.currentTask = None
# 判断任务是否为空
def busy(self):
if self.currentTask != None:
return True
else:
return False
def startNext(self, newTask):
self.currentTask = newTask
self.timeRemaining = newTask.getPages() * 60 / self.pagerate
#统一成一分钟一页,然后有多少页再去处理他的速度
class Task:
def __init__(self, current_time):
self.timestamp = current_time
# 生成时间戳
self.pages = random.randrange(1, 21)
# 打印页数
def getStamp(self):
return self.timestamp
# 获取时间
def getPages(self):
return self.pages
# 获取页数
def waitTime(self, currentTime):
return currentTime - self.timestamp
# 等待时间
def newPrintTask():
# 打印任务函数
num = random.randrange(1, 181)
# 1/180的概率生成作业
if num == 180:
return True
else:
return False
def simulation(numSeconds, pagesPerMinute):
# 模拟函数,numSeconds是模拟多长时间 pagesPerMinute是打印机处理速度
labprinter = Printer(pagesPerMinute)
# 实例化Printer对象并将打印速度传递进去
printQueue = Quque()
# 实例化队列对象
waitingtimes = []
# 声明一个列表waitingtimes
for currentSecond in range(numSeconds):
# 在模拟时长里面一直循环里面的计划
if newPrintTask():
task = Task(currentSecond)
printQueue.enqueue(task)
# 把本次任务压入队列
#
if (not labprinter.busy()) and (not printQueue.isEmpty()):
#如果打印机空闲 并且 队列有任务
nexttask = printQueue.dequeue()
#让排队的队列头弹出队列
waitingtimes.append(nexttask.waitTime(currentSecond))
# 记录这个打印任务在队列中等待了多长时间
labprinter.startNext(nexttask)
#把任务送入打印函数进行打印处理
labprinter.tick()
if len(waitingtimes) > 0:
averageWait = sum(waitingtimes) / len(waitingtimes)
print("average wait %6.2f secs %3d tasks remaining ." % (averageWait, printQueue.size()))
print(waitingtimes)
else:
print("没有打印任务,无法计算平均等待时间。")
for i in range(10):
simulation(3600, 5)
思路说明:
在这个模拟打印机里,每模拟一秒,都会有1/180的概率生成一个打印任务,诺有打印任务,则判断打印机是否空闲,且列队是否有打印任务,如果都符合,列队弹出排队的打印任务,然后完成打印,打印任务生成后开始记录排队时间,打印完毕后排队时间结束并记录
双端队列的抽象数据结构
双端队列是一种可以在两端进行插入和删除操作的数据结构。它的名字来自于 Double-Ended Queue 的缩写。
双端队列的基本操作如下
| 操作 | 功能描述 |
|---|---|
Deque() | 创建一个空双端队列 |
addFront(item) | 将 item 加入队首 |
addRear(item) | 将 item 加入队尾 |
removeFront() | 从队首移除数据项,返回值为移除的数据项 |
removeRear() | 从队尾移除数据项,返回值为移除的数据项 |
isEmpty() | 返回 deque 是否为空 |
size() | 返回 deque 中包含数据项的个数 |

用python实现抽象数据类型deque
class Deque:
#初始化空双端队列
def __init__(self):
self.items = []
#判断队列是否为空
def isEmpty(self):
return self.items == []
#将item加入队首
def addFront(self, item):
self.items.append(item)
#将item加入队尾
def addRear(self, item):
self.items.insert(0, item)
#从队首移除数据项
def removeFront(self):
return self.items.pop()
#从队尾移除数据项
def removeRear(self):
return self.items.pop(0)
#返回队列长度
def size(self):
return len(self.items)
双端队列的基础应用举例
def palchecker(aString):
#初始化双端队列
chardeque = Deque()
# 将输入的文本全部从队尾加入到双端队列中
for i in aString:
chardeque.addRear(i)
#默认为正确的回文
stillEqual = True
while chardeque.size() > 1 and stillEqual == True:
first = chardeque.removeRear()
last = chardeque.removeFront()
#从队首和队尾分别取出一个数据项进行比对,如果相同则继续比对,如果仅剩余一个直接推出显示True
if first != last:
stillEqual = False
return stillEqual
print(palchecker("radar"))
#回文显示
**思路说明:**先把要判断回文的字符穿压入队列,然后进行循环,如果队列大于1并且还是回文的情况下,进行队首和队尾的数据项进行匹配,如果不匹配则等于False
无序表
无序表是一种线性数据结构,其中的元素按照插入的先后顺序排列,而不是按照元素值的大小排序,可以简单理解为没有排序的list数据类型
无序表更像一个购物清单
你按照想到的顺序写下要买的东西:
1. 牛奶
2. 面包
3. 苹果
4. 可乐
真不想写基本操作了,大家知道有这么个东西就行了😓😓😓算了还是写出来吧
无序表的基本操作
| 操作 | 功能描述 |
|---|---|
List() | 创建一个空列表 |
add(item) | 添加一个数据项到列表中,假设 item 原先不存在于列表中 |
remove(item) | 从列表中移除 item,列表被修改,item 原先应存在于表中 |
search(item) | 在列表中查找 item,返回布尔类型值 |
isEmpty() | 返回列表是否为空 |
size() | 返回列表包含了多少数据项 |
append(item) | 添加一个数据项到表末尾,假设 item 原先不存在于列表中 |
index(item) | 返回数据项在表中的位置 |
insert(pos,item) | 将数据项插入到位置 pos,假设 item 原先不存在于列表中,同时原列表具有足够多个数据项,能让 item 占据位置 pos |
pop() | 从列表末尾移除数据项,假设原列表至少有1个数据项 |
pop(pos) | 移除位置为 pos 的数据项,假设原列表存在位置 pos |
有序表
有序表是一种线性数据结构,其中的元素按照某种排序规则(通常是升序或降序)排列,而这种顺序是由元素的值的大小决定的,而不是由插入顺序决定的。
有序表的基本操作:
| 操作 | 功能描述 |
|---|---|
OrderedList() | 创建一个空的有序表 |
add(item) | 在表中添加一个数据项,并保持整体顺序,此项原不存在 |
remove(item) | 从有序表中移除一个数据项,此项应存在,有序表被修改 |
search(item) | 在有序表中查找数据项,返回是否存在 |
isEmpty() | 判断是否为空表 |
size() | 返回表中数据项的个数 |
index(item) | 返回数据项在表中的位置,此项应存在 |
pop() | 移除并返回有序表中最后一项,表中应至少存在一项 |
pop(pos) | 移除并返回有序表中指定位置的数据项,此位置应存在 |
链表
链表(Linked List)是一种线性数据结构,和 Python 列表(list)不同,它的元素在内存里不是连续存放的,而是通过 “节点” 和 “指针” 串联起来的。
链表的基本单位是节点,每个节点包含:
- 数据域:存储本节点的数据
- 指针域:存储下一个节点的地址
其中有两个东西比较重要,就是链表头和链表尾
1. 链表头(头节点 / 头指针)
- 它的指针域指向链表的第一个节点
- 如果是 “带头节点” 的链表,头节点本身不存数据,只存指针
- 如果是 “不带头节点” 的链表,头指针直接指向第一个数据节点
2.链表尾(尾节点)
- 尾节点的指针域必须指向
None(空),用来表示链表结束 - 尾节点依然包含本节点的数据,只是它的 “下一个节点地址” 是空的

链表的基本操作
| 操作名称 | 功能说明 |
|---|---|
LinkedList() | 创建一个空链表 |
add(item) | 在链表头部添加新节点 |
append(item) | 在链表尾部添加新节点 |
insert(pos, item) | 在指定位置 pos 插入节点 |
remove(item) | 删除指定数据的节点 |
search(item) | 查找元素是否存在,返回布尔值 |
isEmpty() | 判断链表是否为空 |
size() | 返回链表节点个数 |
traverse() | 遍历并输出链表所有元素 |
pop() | 删除并返回尾部节点 |
非线性结构
散列表
散列表又称哈希表,是一种数据集,其中数据项的存储方式非常有利于快速查找定位
散列表钟的每一个存储位置,成为槽(slot),可以用来保存数据项,每个槽有一个唯一的名字
这里有一个常用的散列方式就是**求余数**,将数据项除以散列的大小,得到的余数作为槽号
这里需要注意,求余的结果需要限制在散列表的范围内,这里最保险的方式就是除以散列表的大小

比如这个 item // 11 == hash Value,这里就有六个数据项插入了散列表的槽位
槽被数据项占据的比例称为散列表的"负载因子",这里的负载因子为6//11
如果还要保存一个44,这里的化 44 // 11就是0,如果为0的化会和77占一个槽位,这种情况下我们称之为冲突
如果有一组数据项,如果一个散列函数可以把每个数据项映射到不同的槽中,那么这个散列函数可以称之为**完美散列函数**
一个好的散列函数需要具备:
- 冲突最少(近似完美)
- 计算难度低(额外开销小)
- 充分分散数据项(节约空间)
最著名的完美散列函数是MD5和SHA系列函数
MD5可以将任何长度的数据变换为固定长度为128位的摘要
- 在python中有自带的散列函数库:hashlib
- hashlib.md5()
区块链(散列函数的应用)
本质上来讲,区块链就是分布式的数据库,通过网络链接的节点,每个节点都保存着整个数据库所有数据,任何地点存入的数据都会完成同步,区块链的最本质特征就是去中心化,不存在任何控制节点,协调中心节点,所有节点都是平等的,无法被控制
- 区块链由一个个区块(block)组成,区块分为头(head)和体(body)
- 区块头记录了一些元数据和链接到前一个前一个区块的信息(生成时间,前一个区块头+区块体的散列值)

散列函数设计
-
折叠法设计散列函数
将数据项按照位数分为诺干段,再将几段数字相加,最后对散列表大小求余,得到散列值
有时折叠法会包含格数反转的步骤
-
平方取中法
首先将数据项做平方运算,然后取平方数中间两位,再对散列表的大小进行求余
我们还可以设计出更多的散列函数方法,但要坚持的出发点是,散列函数不能称为存储过程和查找过程的计算负担
散列表冲突的解决方案
以上提到的完美散列函数,往往是不现实的,这时候我们就需要解决散列表冲突的问题
开放寻址法
解决散列表冲突的一种方法就是为冲突的数据项再找一个开放的空槽来保存
1.线性探测
最简单的就是从冲突的槽开始往后扫描,直到碰到一个空槽,如果到散列表尾部还未找到,则从首部接着扫描
这种寻找空槽的技术称之为开放定址,往后逐个槽寻找的方法则是开放定址技术中的线性探测

线性探测的缺点就是有聚集的趋势,如果一个槽冲突的数据项比较多的化,这些数据项就会在槽的附件聚集起来,从而连锁式的影响其他数据项的插入
2.跳跃式探测
未来避免聚集,可以尝试吧线性探测扩展,也就是采用跳跃式探测,也就是每次+x进行探测插入
+3 探测44,55,20

3.二次探测
不再是固定的skip值,而是逐步递增例如skip的值变成 1 3 5 7 9
再散列
rehash(pos) = (pos+skip) % sizeoftable
其中skip是跳跃步长,sizeoftable是散列表的总槽位数
注意skip不能被散列表的大小整除,否则会无限循环
数据项链
除了上面说的,寻找空槽的开放定址技术外,另一种解决散列冲突的方案是将容纳单个数据项的槽扩展为容纳数据项的集合
映射ADT Map
python的数据类型之一字典大家还记得吗,字典是一种可以保存key-data键值对的数据类型,这种键值关联的方式称为**映射map**
映射的基本操作如下:
| 操作 | 功能描述 |
|---|---|
Map() | 创建一个空映射,返回空映射对象 |
put(key, val) | 将key-val关联对加入映射中,如果key已存在,将val替换旧关联值 |
get(key) | 给定key,返回关联的数据值,如不存在,则返回None |
del | 通过del map[key]的语句形式删除key-val关联 |
len() | 返回映射中key-val关联的数目 |
in | 通过key in map的语句形式,返回key是否存在于关联中(布尔值) |
用python实现抽象数据类型ADT Map
class HashTable:
def __init__(self):
self.size = 11 # 初始化定义映射长度
self.slots = [None] * self.size #初始化key容器
self.data = [None] * self.size #初始化value容器
#根据key值,寻找映射槽位
def hashfunction(self,key):
return hash(key) % self.size #让hash值在映射长度里,不会超出映射长度
#如果目标槽位有key,则会触发该函数,自动槽位+1
def rehash(self,oldhash):
return (oldhash + 1) % self.size
def put(self,key,data):
hashValue = self.hashfunction(key) #把key要存放的槽位放在hashValue变量中
#如果目标槽位的key是空的则直接把当前的key和value放入槽位
if self.slots[hashValue] == None:
self.slots[hashValue] = key
self.data[hashValue] = data
else: #这个槽被人占了则进行如下判断
#情况1:key目标槽位的值和key是一样的,则直接把data替换掉
if self.slots[hashValue] == key:
self.data[hashValue] = data
else:
# 情况2,key目标槽位和key的值不一样,则使用开放寻址法
nextslot = self.rehash(hashValue) #nextslot 是原槽位的下一个位置(线性探测)
while self.slots[nextslot] != None and self.slots[nextslot] != key:
# 如果下压根位置不等于空,并且key的值也不相同,则槽位去下一个位置
nextslot = self.rehash(nextslot)
if self.slots[nextslot] == None:
#如果是空的则直接key和data同时存入
self.slots[nextslot] = key
self.data[nextslot] = data
else:
#如果key相同则把数据进行替换
self.data[nextslot] = data
def get(self,key):
startslot = self.hashfunction(key)
#把key对应的槽放入startslot
data = None #最终结果
stop = False #防止死循环
found = False #标记是否找到
position = startslot
while self.slots[position] != None and found == False and stop == False:
#如果key的槽不为空,并且没有找到标记,而且stop不停的情况下,则一直循环
if self.slots[position] == key:
#如果key值正好就和key列表的key值一样,则标记找到了,然后取出value
found = True
data = self.data[position]
else:
#如果key不相等则槽位向后找
position = self.rehash(position)
if position == startslot:
#什么时候找一圈都找不到,则结束循环
stop = True
return data
#魔术方法实现可以用[]访问和修改
def __getitem__(self,key):
return self.get(key)
def __setitem__(self,key,data):
self.put(key,data)
树
树是一种非线性数据结构,它模拟了现实世界中层次关系,比如公司组织架构、电脑文件目录,和自然界的树一样,数据结构树也分为:根,枝,叶
分类树的特征:
- 层次化:约到顶部越普遍,越到底部越独特
- 隔离性:一个节点的子节点,和另外一个节点的子节点是相互隔离的,独立的
- 唯一性:每个叶节点都具有唯一性
树的基本相关术语
-
节点:树中的每个元素称为节点。
-
边:连接两个节点的线,表示它们之间的父子关系。
-
路径:从一个节点到另一个节点所经过的节点序列(按边的顺序)。
例如:html->head->meta
-
根:树中最顶层的节点,没有父节点。
-
兄弟节点:具有相同父节点的节点。
-
子树:树中某个节点及其所有后代节点构成的一棵小树。
-
叶节点:没有子节点的节点。
-
层级:节点所在的层级,根所在的层级通常定义为 0 或 1
-
高度:树的高度 = 根节点的高度

树由诺干节点,以及两两链接的边组成,其中一个节点被设定为根,每个节点n都有一条来自节点p的边,p是n的父节点,每个节点从根开始的路径都是唯一的,如果每个节点最多有两个子节点,则称之为二叉树
树的嵌套列表实现

def BinaryTree(r):
return [r,[],[]]
def insertLeft(root,newBranch):
t = root.pop(1)
if len(t) > 1:
root.insert(1,[newBranch,t,[]])
else:
root.insert(1,[newBranch,[],[]])
return root
def insertRight(root,newBranch):
t = root.pop(2)
if len(t) > 1:
root.insert(2, [newBranch, [], t])
else:
root.insert(2, [newBranch, [], []])
return root
def getRootVal(root):
return root[0]
def setRootVal(root,newval):
root[0] = newval
def getLeftChild(root):
return root[1]
def getRightChild(root):
return root[2]
树的链表实现

每个节点保存根节点的数据项,以及指向左右子树的链接
class BinaryTree:
def __init__(self,rootObj):
self.key = rootObj
self.leftChild = None
self.rightChild = None
#初始化,左右子树均为空,只有ROOT树顶
def insertLeft(self,newNode):
#如果左子节点没有数据,则创建一个新节点,然后插入到父节点的左子节点
if self.leftChild == None:
self.leftChild = BinaryTree(newNode)
else:
#如果左子节点有数据,则把旧数据放在新子节点的左下方
t = BinaryTree(newNode)
t.leftChild = self.leftChild
self.leftChild = t
def insertRight(self,newNode):
# 如果右子节点没有数据,则创建一个新节点,然后插入到父节点的右子节点
if self.rightChild == None:
self.rightChild = BinaryTree(newNode)
# 如果右子节点有数据,则把旧数据放在新子节点的右下方
else:
t = BinaryTree(newNode)
t.rightChild = self.rightChild
self.rightChild = t
def getRightChild(self):
return self.rightChild
def getLeftChild(self):
return self.leftChild
def setRootVal(self,obj):
self.key = obj
树的应用
- 表达式解析树
def BinaryTree(r):
return [r,[],[]]
def insertLeft(root,newBranch):
t = root.pop(1)
if len(t) > 1:
root.insert(1,[newBranch,t,[]])
else:
root.insert(1,[newBranch,[],[]])
return root
def insertRight(root,newBranch):
t = root.pop(2)
if len(t) > 1:
root.insert(2, [newBranch, [], t])
else:
root.insert(2, [newBranch, [], []])
return root
def getRootVal(root):
return root[0]
def setRootVal(root,newval):
root[0] = newval
def getLeftChild(root):
return root[1]
def getRightChild(root):
return root[2]
# 树操作函数
# 定义栈的类
class Stack:
def __init__(self): # 初始化,创建一个空列表来存储栈元素
self.items = []
def isEmpty(self): # 判断栈是否为空
return self.items == []
def push(self, item): # 将元素压入栈顶
self.items.append(item)
def pop(self): # 弹出栈顶元素
return self.items.pop()
def peek(self): # 检查栈顶元素
return self.items[-1]
def size(self): # 返回栈的大小
return len(self.items)
# 栈操作函数
#创建解析树
def buildParseTree(fpexp):
fplist = fpexp.split()
pStack = Stack()
eTree = BinaryTree('')
pStack.push(eTree)
currentTree = eTree
for i in fplist:
if i == '(':
currentTree = insertLeft('')
pStack.push(currentTree)
currentTree = currentTree.getLeftChild()
elif i not in ['+','-','*','/',')']:
currentTree.setRootVal(int(i))
parent = pStack.pop()
currentTree =parent
elif i in ['+','-','*','/']:
currentTree.setRootVal(int(i))
currentTree = insertRight('')
pStack.push(currentTree)
currentTree = currentTree.getRightChild()
elif i == ')':
currentTree = pStack.pop()
else:
raise ValueError
return eTree
#递归求值
import operator
def evaluate(parseTree):
opers = {'+':operator.add,'-':operator.sub,'*':operator.mul,'/':operator.truediv}
leftC = parseTree.getLeftChild()
rightC = parseTree.getRightChild()
if leftC and rightC:
fn = opers[parseTree.getRootVal()]
return fn(evaluate(leftC),evaluate(rightC))
else:
return parseTree.getRootVal()
树的遍历
- 前序遍历
先访问根节点,在递归地前序访问左子树,最后前序访问右子树
- 中序遍历
先递归的中序访问左子树,在访问根节点,最后中序访问右子树
- 后序遍历
先递归的地后续访问左子树,在后续访问右子树,最后访问根节点
#前序遍历
def preorder(tree):
if tree:
print(tree.getRoot())
preorder(tree.getLeftChild())
preorder(tree.getRightChild())
#后续遍历
def postorder(tree):
if tree:
postorder(tree.getLeftChild())
postorder(tree.getRightChild())
print(tree.getRoot())
#中序遍历
def inorder(tree):
if tree:
inorder(tree.getLeftChild())
print(tree.getRoot())
inorder(tree.getRightChild())
二叉堆
优先队列:优先队列的出队和普通队列一样由队首出列,但是在优先队列内部,数据项的次序是由优先级来确定的
二叉堆本质上是一棵完全二叉树,并且满足一个特殊的堆叠规则:任何一个父节点的值,都比它的两个子节点要大(或要小)。
需要保持平衡完全二叉树,如果二叉树长歪,从叶到根的将会更加费时间复杂度

完全二叉树图解
其中如何从一个无嵌套的数列中找子节点,有如下公式
| 从 0 开始(Python / 你的代码) | 2p + 1 | 2p + 2 | (p - 1) // 2 |
|---|---|---|---|
| 从 1 开始(教科书 / 你老师的版本) | 2p | 2p + 1 | p // 2 |
堆次序
| 类型 | 规则 | 根节点 |
|---|---|---|
| 最小堆 | 父 ≤ 子 (任何节点 ≤ 它的子节点) | 全堆最小 |
| 最大堆 | 父 ≥ 子 (任何节点 ≥ 它的子节点) | 全堆最大 |
当需要插入的时候,把值添加到最左下角,然后依次向上判断**(上浮)**
如果需要删除最大/小值的时候,把根删除,然后把最后的数据排入到根,再和较小的子节点比较,如果比子节点大就交换,然后交换下来的子节点继续下沉,直到满足排序
ADT BinaryHeap(二叉堆)操作定义
| 操作方法 | 功能描述 |
|---|---|
BinaryHeap() | 创建一个空二叉堆对象 |
insert(k) | 将新key加入到堆中 |
findMin() | 返回堆中的最小项,最小项仍保留在堆中 |
delMin() | 返回堆中的最小项,同时从堆中删除 |
isEmpty() | 返回堆是否为空 |
size() | 返回堆中key的个数 |
buildHeap(list) | 从一个key列表创建新堆 |
二叉堆的python实现
class BinHeap:
def __init__(self):
self.heapList = [0] #初始化二叉堆列表为0
self.currentSize = 0 #第一个占位符,以免出现偏移值】
def percUp(self,i):
while i // 2 > 0:
if self.heapList[i] < self.heapList[i // 2]:
#如果子节点比父节点小,则直接交换
tmp = self.heapList[i // 2]
self.heapList[i // 2] = self.heapList[i]
self.heapList[i] = tmp
i = i // 2
#交换完再次指向它的父元素进行比
def insert(self,k):
#把最新的数据插入到元素的最后
self.heapList.append(k)
self.currentSize = self.currentSize + 1
self.percUp(self.currentSize)
def percDown(self,i):
while (i * 2) <= self.currentSize:#判断是否到达最小元素
mc = self.minChild(i)#拿到最小的子节点
if self.heapList[i] > self.heapList[mc]:#进行比对,如果当前节点比最小子节点大,则交换位置
tmp =self.heapList[i]
self.heapList[i] = self.heapList[mc]
self.heapList[mc] = tmp
i = mc #i就是当前最小的节点
def minChild(self,i):
#判断子节点那个值更小
if i * 2 + 1 > self.currentSize: #如果只有一个子节点则直接返回这个子节点
return i * 2
else:
if self.heapList[i * 2] < self.heapList[i * 2 + 1]: #如果有来个子节点, 就比较这两个子节点,那个小,返回那个
return i * 2
else:
return i * 2 + 1
def delMin(self):
#删除最小元素
retval = self.heapList[1]#先把最小元素保存
self.heapList[1] = self.heapList[self.currentSize]#然后把最后的元素值放入根元素
self.currentSize = self.currentSize - 1#列表长度-1
self.heapList.pop()#删除最后一个元素
self.percDown(1)
return retval
def buildHeap(self,alist):
#无序表生成堆
i = len(alist) // 2 #i是二叉树最左下角的数据
self.currentSize = len(alist) #初始化二叉树的列表长度等于无序表的长度
self.heapList = [0] + alist[:]#复制一份无序表给heaplist,并把0插入到第一位
print(len(self.heapList),i)
while(i>0): # 这时候的二叉树有子节点,则让倒数第二层依次下沉,直到下沉完根节点
print(self.heapList,i)
self.percDown(i)
i = i - 1
print(self.heapList,i)
二叉查找树
这里看陈斌老师的课已经看不懂了,换了个地方从新学了一下二叉查找树
核心规则:左子树所有节点的值,必须小于当前节点的值,右子树所有节点的值必须大于当前节点的值,并且这条规则必须对整棵树所有的节点都必须严格成立
平衡树:每个节点左右子树的高度差不超过1
搜索
想搜索一个元素需从根节点开始比较,比当前根元素小就向左,比当前根元素大就向右,递归每深入一层,就压入一层调用栈,找到之后出栈

这里应该还有迭代查找,但是时间有限就不举例子了
插入
首先判断当前的节点是否为空,如果为空,则直接插入到空节点,如果非空,key大于当前节点则把右子节点放入递归,key小于当前节点则把左子节点放入递归

删除
诺要删除一个值,需要从根节点递归查找,小于就去左子树查,大于就去右子树查找,直到找到,之后会面临三种情况:
- 没有孩子元素(叶子节点),则直接删除该节点
- 没用左子节点,或者没有右子节点,这样删除当前节点后,直接把唯一的子树替换掉当前节点就可以了
- 如果同时有左右子树,找到它的中序后继(右子树的最小节点),用这个节点的值替换当前节点的值,再去右子树删除当前的最小节点

二叉查找树的python实现
有点多这里就不写了,好兄弟百度一下吧
AVL树
因为二叉查找树在插入的时候压根不会考虑到层级平衡的问题,只会考虑到比较大小然后插入到叶子节点,然后我们引入了avl树
在avl树的实现中,需要对每个节点跟踪一个参数叫做平衡因子
平衡银子是根据节点的左右子树的高度来定义的,确切来说是左右子树的高度差,也就是左树高度减去右树高度
如果平衡因子大于0,称为左重,如果平衡因子小于0则称为右重,如果平衡因子等于0,则称为平衡
如果一个二叉树的每个节点的平衡因子都在-1,1,0之间我们就称之为平衡树

在平衡树操作过程中,有节点的平衡因子超出范围,则会有一个重新平衡的过程(保持BST的性质)
平衡过程:
1. 插入位置向上回溯,找到第一个 `|BF| = 2` 的节点,记为 失衡节点 X
2. `BF(X) = +2` → 左重,说明问题在左子树,相反`BF(X) = -2 → 右重,说明问题在右子树
3. 如果 `BF(X) = +2`(左重),去看 X.left 的平衡因子
- `BF(X.left) = +1` → 左左情况(LL)→ 右旋
- `BF(X.left) = -1` → 左右情况(LR)→ 先左旋再右旋
4. 如果 `BF(X) = -2`(右重),去看 X.right 的平衡因子
- `BF(X.right) = -1` → 右右情况(RR)→ 左旋
- `BF(X.right) = +1` → 右左情况(RL)→ 先右旋再左旋
右旋:把失衡节点的左孩子,提上来当新根;再把这个左孩子原来的右子树,挂回失衡节点的左边
左旋:把失衡节点的右孩子,提上来当新根;再把这个右孩子原来的左子树,挂回失衡节点的右边
来几张图给兄弟们举个例子吧
右旋:


先左旋再右旋

avl平衡树的python实现
给avl树插入新值,新值肯定会出现在叶节点,这时,如果插入在父节点的左子树,父节点的BF+1,如果插入在父节点的右子树,父节点的BF-1,这种影响可能随着父节点的路径一路传递上去,直到:
- 传递到根节点为止
- 或者某个父节点平衡因子为0,不再影响上层的平衡因子为止
真不想手撕avl树了,你们在网上搜一下相关代码吧,我肝不动了:(
图
- 图是数学和计算机科学中的一个核心概念,它是一种用于描述实体与实体之间关系的数据结构或模型
图=节点+边
- 边:作为两个节点的关系表示,边链接两个顶点,边可以是无向的或者是有向的,相应的图称作"无向图"和“有向图”
- **节点:**图的基本组成部分,顶点具有名称表示key,也可以携带数据项value
- 权重:一个节点到另外一个节点所需要的代价,例如:公交网络重两个站点之间的距离,通行时间,票价都可以作为权重,这种带权重的图,我们称之为
赋权图 - **路径:**图中的路径,是由边依次链接起来的顶点序列,无权路径的长度为边的数量,有权路径的长度为所有边权重的和
- **圈:**圈是首尾顶点相同的路径,如下图v5 v2 v3 v5是一个圈,如果一共图重没用任何圈,则称作有向无圈图

图的基础操作
| 方法 / 操作 | 功能描述 |
|---|---|
Graph() | 创建一个空的图 |
addVertex(vert) | 将顶点 vert 加入图中 |
addEdge(fromVert, toVert) | 添加有向边 |
addEdge(fromVert, toVert, weight) | 添加带权的有向边 |
getVertex(vKey) | 查找名称为 vKey 的顶点 |
getVertices() | 返回图中所有顶点列表 |
in(vert in graph) | 按照 vert in graph 的语句形式,返回顶点是否存在图中(True/False) |
图的实现方法
- 邻接矩阵
- 邻接矩阵的每行和每列都代表图重的顶点
- 如果两个顶点之间有边项链,则设定权重值
- 无权边则将矩阵标注为1,无边则设置为0,如果带权边则将权重保存为矩阵的分量值

- 邻接列表
邻接列表需要维护一个包含所有顶点的主列表,主列表中的每个顶点再关联一共与自身有边链接的所有顶点的列表,例如下图v0顶点链接的信息指向本id是V0,指向V1和V5有边,并且标出了权重

数据结构图的python实现
class Vertex:
#初始化节点和节点绑定的信息
def __init__(self,key):
self.id = key
self.connectedTo = {}
#增加邻居以及权重
def addNeighbour(self,neighbour,weight=0):
self.connectedTo[neighbour] = weight
#当你打印一个对象时,Python 会自动调用这个对象的 __str__() 方法,来决定打印出来的文字是什么,打印对象的节点以及边指向的节点
def __str__(self):
return str(self.id) + 'connectedTo:' + str([x.id for x in self.connectedTo])
#获得邻居节点
def getConnections(self):
return self.connectedTo.keys()
#获取本节点的ID
def getId(self):
return self.id
#返回当前顶点到指定邻居顶点的边的权重。
def getWeight(self,nbr):
return self.connectedTo[nbr]
class Graph:
#数据结构图的参数,节点有0个,节点的字典有0个
def __init__(self):
self.vertices = {}
self.numVertices = 0
#添加节点,节点数量+1,新节点触发创建节点类,并把新节点放入节点字典中并返回新节点的关系
def addVertex(self,key):
self.numVertices = self.numVertices + 1
newVertex = Vertex(key)
self.vertices[key] = newVertex
return newVertex
#根据提供的信息寻找节点,如果找到则返回节点对象,未找到则返回None
def getVertex(self,n):
if n in self.vertices:
return self.vertices[n]
else:
return None
#查看节点字典中是否包含目标节点,
def __contains__(self,n):
return n in self.vertices
#添加边如果其中f是起始节点,t是目标节点,cost权重(默认是0)
def addEdge(self,f,t,cost=0):
#如果没有起始节点或者目标节点则创建这些节点
if f not in self.vertices:
nv = self.addVertex(f)
if t not in self.vertices:
nt = self.addVertex(t)
#然后再调用addNeighbour方法添加边
self.vertices[f].addNeighbour(self.vertices[t],cost)
#
def getVertices(self):
return self.vertices.keys()
def __iter__(self):
return iter(self.vertices.values())
广度优先算法BFS
广度优先搜索是一种逐层向外扩展的图遍历算法,像水面投石后激起的涟漪一样,从起点开始,一层一层地向外扩散。简单来说先访问离起点最近的节点,再逐步向外层扩展
-
为了跟踪顶点的加入过程,并避免重复顶点,要为顶点加入三个属性
- 距离:起始顶点到此顶点的路径长度
- 前驱顶点:可以反向追溯到起点
- 颜色:标识了此顶点尚未发现(白色)已经发现(灰色)还是已经完成探索(黑色)
-
还需要用一个队列来对已发现的顶点进行排序
- 决定下一个要探索的顶点
BFS算法过程:
从起始顶点S开始,作为刚发现的顶点,标注为灰色,距离为0,前驱为None,加入队列,然后就是开始遍历以下内容
从队首取出一个顶点作为当前顶点,遍历当前顶点的邻接顶点,如果尚未发现是白色顶点,则将其颜色改变为灰色(已发现),距离增加1,前驱顶点为当前顶点,加入到队列中遍历完成后,将当前顶点设置为黑色(已探索过),循环回到步骤1的队首取当前顶点
广度优先算法的例子:词梯问题
class Queue:
# 初始化空队列
def __init__(self):
self.items = []
# 判断队列是否为空
def isEmpty(self):
return self.items == []
# 判断队列内的个数
def size(self):
return len(self.items)
# 压入队列尾部
def enqueue(self, item):
self.items.insert(0, item)
# 弹出队列头
def dequeue(self):
return self.items.pop()
class Vertex:
# 初始化节点和节点绑定的信息
def __init__(self, key):
self.id = key
self.connectedTo = {}
# BFS 需要的属性
self.color = 'white' # white:未访问, gray:已发现, black:已处理
self.distance = 0 # 从起点到当前节点的距离
self.pred = None # 前驱节点(路径中的上一个节点)
# 增加邻居以及权重
def addNeighbour(self, neighbour, weight=0):
self.connectedTo[neighbour] = weight
# 打印对象时调用
def __str__(self):
return str(self.id) + ' connectedTo: ' + str([x.id for x in self.connectedTo])
# 列出全部邻居节点
def getConnections(self):
return self.connectedTo.keys()
# 获取本节点的ID
def getId(self):
return self.id
# 返回当前顶点到指定邻居顶点的边的权重
def getWeight(self, nbr):
return self.connectedTo[nbr]
# ----- BFS 需要的 getter/setter 方法 -----
def getColor(self):
return self.color
#获取颜色
def setColor(self, color):
self.color = color
#设置颜色
def getDistance(self):
return self.distance
#获得距离
def setDistance(self, dist):
self.distance = dist
#设置距离
def getPred(self):
return self.pred
#获得前驱节点
def setPred(self, pred):
self.pred = pred
#设置前驱节点
class Graph:
# 数据结构图的参数,节点有0个,节点的字典有0个
def __init__(self):
self.vertices = {}
self.numVertices = 0
# 添加节点
def addVertex(self, key):
self.numVertices = self.numVertices + 1
newVertex = Vertex(key)
self.vertices[key] = newVertex
return newVertex
# 根据提供的信息寻找节点
def getVertex(self, n):
if n in self.vertices:
return self.vertices[n]
else:
return None
# 查看节点字典中是否包含目标节点
def __contains__(self, n):
return n in self.vertices
# 添加边
def addEdge(self, f, t, cost=0):
if f not in self.vertices:
self.addVertex(f)
if t not in self.vertices:
self.addVertex(t)
self.vertices[f].addNeighbour(self.vertices[t], cost)
# 获取所有节点id
def getVertices(self):
return self.vertices.keys()
# 遍历图中所有的顶点对象
def __iter__(self):
return iter(self.vertices.values())
def buildGraph(wordFile):
#创建一个空字典用来存储桶
d = {}
# 生成空的图用来存储整个单词关系图
g = Graph()
#打开wordFile文件
with open(wordFile, 'r') as wfile:
for line in wfile:
#去除这个文件的制表符,并把每行数据写入word
word = line.strip()
#对每行数据进行遍历,让每个字母都称为一次通配符
for i in range(len(word)):
bucket = word[:i] + "." + word[i + 1:]
#共4次,例入是FOOL,则会是.OOL F.OL FO.L FOO.四个
if bucket in d: #如果当前的有桶则放入桶里
d[bucket].append(word)
else:##如果当前没用桶则创建桶
d[bucket] = [word]
for bucket in d.keys():
#遍历出所有的桶
for word1 in d[bucket]:
# 让这个元素的第一个单词和后面的单词依次比较,如果不相等则加入个边
for word2 in d[bucket]:
if word1 != word2:
g.addEdge(word1, word2)
return g
#把同一个桶里的所有单词两两之间连上边
def bfs(g, start):
"""广度优先搜索"""
#初始化距离和前驱节点
start.setDistance(0) #从起点到当前节点的距离(步数)
start.setPred(None) # 前驱节点(路径中的上一个节点)
vertQueue = Queue() #实例化Queue队列
vertQueue.enqueue(start) #起点加入队列
while vertQueue.size() > 0:
#如果队列里有内容
currentVert = vertQueue.dequeue()
#弹出队列头部并写入currentVert
for nbr in currentVert.getConnections():
#开始遍历弹出节点的全部的邻居节点
if nbr.getColor() == 'white':
#如果当前的邻居节点颜色是白色(未发现),则标记为灰色(已发现)
nbr.setColor('gray')
nbr.setDistance(currentVert.getDistance() + 1) #让当前的距离+1
nbr.setPred(currentVert) #并把前驱顶点设置弹出的节点
vertQueue.enqueue(nbr) #把白色(未发现)的邻居节点加入到队列中
currentVert.setColor('black')#然后把弹出队列的节点标记为已完成探索
def traverse(y):
"""从目标节点回溯打印路径"""
x = y
#x作为备份
path = []
#初始化最短路径的列表为空
while x.getPred():
# 开始从目标节点一个一个向前寻找前驱节点
path.append(x.getId())
#把当前节点的id加入到列表
x = x.getPred() #然后x=x.getPred()再去寻找前一个的前驱节点
#最后把起点也加入到列表
path.append(x.getId())
path.reverse() # 反转得到从起点到终点的顺序
print(" -> ".join(path))
#从起点到终点用->链接
if __name__ == "__main__":
# 构建图
print("\n正在构建图...")
wordgraph = buildGraph('words.txt')#根据words文件创建的单词图
print(f"图中顶点数: {wordgraph.numVertices}")
# 设定起点和终点
start_vertex = wordgraph.getVertex('FOOL')
end_vertex = wordgraph.getVertex('SAGE')
#如果起点或者终点没在单词表则报错,如果在则直接执行广度优先搜索并返回最短路径
if start_vertex is None or end_vertex is None:
print("错误:FOOL 或 SAGE 不在单词列表中")
else:
# 执行 BFS,传参图和起点
print(f"\n从 FOOL 到 SAGE 的 BFS 搜索...")
bfs(wordgraph, start_vertex)
# 打印路径
print("\n最短路径:")
traverse(end_vertex)
深度优先搜索DFS
相比广度优先搜索这种逐层建立搜索树的特点,深度优先搜搜是沿着树的单支尽量深入向下搜索,如果到无法继续的程度还是未找到问题的解法,就回溯到上一层再搜索下一支树
骑士周游问题的深度优先搜索和启发式规则的例题
class Vertex:
#初始化节点和节点绑定的信息
def __init__(self,key):
self.id = key
self.connectedTo = {}
self.color = 'white'
#增加邻居以及权重
def addNeighbour(self,neighbour,weight=0):
self.connectedTo[neighbour] = weight
#当你打印一个对象时,Python 会自动调用这个对象的 __str__() 方法,来决定打印出来的文字是什么,打印对象的节点以及边指向的节点
def __str__(self):
return str(self.id) + 'connectedTo:' + str([x.id for x in self.connectedTo])
#获得邻居节点
def getConnections(self):
return self.connectedTo.keys()
#获取本节点的ID
def getId(self):
return self.id
#返回当前顶点到指定邻居顶点的边的权重。
def getWeight(self,nbr):
return self.connectedTo[nbr]
def getColor(self): # ✅ 新增:获取颜色
return self.color
def setColor(self, color): # ✅ 新增:设置颜色
self.color = color
class Graph:
#数据结构图的参数,节点有0个,节点的字典有0个
def __init__(self):
self.vertices = {}
self.numVertices = 0
#添加节点,节点数量+1,新节点触发创建节点类,并把新节点放入节点字典中并返回新节点对象
def addVertex(self,key):
self.numVertices = self.numVertices + 1
newVertex = Vertex(key)
self.vertices[key] = newVertex
return newVertex
#根据提供的信息寻找节点,如果找到则返回节点对象,未找到则返回None
def getVertex(self,n):
if n in self.vertices:
return self.vertices[n]
else:
return None
#查看节点字典中是否包含目标节点,
def __contains__(self,n):
return n in self.vertices
#添加边如果其中f是起始节点,t是目标节点,cost权重(默认是0)
def addEdge(self,f,t,cost=0):
#如果没有起始节点或者目标节点则创建这些节点
if f not in self.vertices:
nv = self.addVertex(f)
if t not in self.vertices:
nt = self.addVertex(t)
#然后再调用addNeighbour方法添加边
self.vertices[f].addNeighbour(self.vertices[t],cost)
#
def getVertices(self):
return self.vertices.keys()
def __iter__(self):
return iter(self.vertices.values())
def genLegalMoves(x,y,dbSize):
# 合法走棋位置函数
newMoves = []#收集当前骑士从 (x, y) 位置出发,所有合法的下一步棋位置。
moveOffsets = [(-1,-2),(-1,2),(-2,-1),(-2,1),
(1,-2),(1,2),(2,-1),(2,1)]
#列出骑士的所有可以尝试走的位置
for i in moveOffsets: #开始尝试每个位置是否可走,并把当前骑士的坐标加上当前的偏移量
newX = x+ i[0]
newY = y + i[1]
if legalCoord(newX,dbSize) and legalCoord(newY,dbSize):#如果x和y都在棋盘里则可以走,把这个函数列入newMoves,作为下次可以走的位置
newMoves.append((newX,newY))
return newMoves #把下一步棋的位置返回
def legalCoord(x,dbSize):
#确保棋子的下一步在棋盘里
if x >= 0 and x < dbSize:
return True
else:
return False
def knightGraph(dbSize):
#构建走棋关系图
ktGraph = Graph()
#建立空图
for row in range(dbSize):
for col in range(dbSize):
#双重循环遍历每个格子,row是行,col是列
nodeId = posToNodeId(row,col,dbSize)#给当前格子添加上唯一标识
newPositions = genLegalMoves(row,col,dbSize)#生成当前位置的可走位置
for e in newPositions:
#遍历当前位置可以走的全部位置
nid = posToNodeId(e[0],e[1],dbSize)#给目标格子添加上唯一id
ktGraph.addEdge(nodeId,nid)#给当前格子和目标格子添加边
return ktGraph#遍历完全部可走位置后返回图,并继续循环本行的下一列
def posToNodeId(row,col,dbSize):
return row * dbSize + col #唯一标识=行*棋盘大小+列
def knightTour(n,path,u,limit): #n:层次 path路径 u当前顶点 limit搜索总深度
#骑士周游算法代码
u.setColor('gray')#把当前节点 u 标记为"已发现
path.append(u)#当前顶点加入路径
if n < limit:#如果没用到达最后一个层次,则继续执行下面程序,否则视为周游完毕退出算法
nbrList = orderByAvail(u)#把全部的邻居节点变化为列表
i = 0
done = False
while i < len(nbrList) and not done:#遍历全部邻居节点
#如果邻居节点是白色未发现,则根据这个白色继续向深探索
if nbrList[i].getColor() == 'white':
done = knightTour(n+1,path,nbrList[i],limit)
i = i + 1
if not done:
path.pop()
u.setColor('white')
else:
done = True
return done
def orderByAvail(n):
resList = []
for v in n.getConnections():
if v.getColor() == 'white':
c = 0
for w in v.getConnections():
if w.getColor() == 'white':
c = c + 1
resList.append((c,v))
resList.sort(key=lambda x: x[0])
return [y[1] for y in resList ]
if __name__ == "__main__":
dbSize = 8 # 8×8 棋盘
limit = dbSize * dbSize - 1 # 63,走完 64 格
# 1. 构建骑士周游图
print("正在构建骑士周游图...")
kg = knightGraph(dbSize)
print(f"图构建完成,共 {kg.numVertices} 个顶点")
# 2. 选择起点(比如左上角 (0,0),对应节点 id=0)
start_vertex = kg.getVertex(0)
# 3. 执行 DFS 回溯
print("正在搜索骑士周游路径...")
path = []
success = knightTour(0, path, start_vertex, limit)
# 4. 输出结果
if success:
print(f"找到路径!长度: {len(path)}")
print(" -> ".join(str(v.getId()) for v in path))
else:
print("未找到路径")
启发式规则
启发式规则是一种“基于经验、直觉或近似方法”来解决问题的策略,而不是保证一定找到最优解的严格数学算法,简单说就是:启发式 = 凭经验走的“捷径”或“经验法则”。
举个例子:
收拾行李箱的时候
- 精确算法:计算所有物品排列组合,找空间利用率最高的方案 → 计算量爆炸
- 启发式规则:大件先放,小件塞缝隙 → 经验有效,不一定最优但很快
递归
具体来说,递归不是一种具体的算法,而是一种解决问题的编程技巧/思想。
-
什么是递归?
递归指的是:一个函数(或方法)在定义中直接或间接地调用自身。精髓在于**
将问题分解为规模更小的相同问题**
递归三定律
- 递归算法必须有一个基本结束条件(
最小规模问题的直接解决) - 递归算法必须能改变状态,向基本结束条件演进(
减小问题规模) - 递归算法必须调用自身(
解决减小了规模的相同问题)
递归的简单应用举例
num = 0
def listsum(numList):
if len(numList) == 1:
return numList[0]
else:
num = numList[0] + listsum(numList[1:])
return num
print(listsum([1,3,5,7,9]))
# 递归列表相加
思路说明: 递归计算列表和:每次取第一个元素,加上剩余列表的和;剩余列表用同样的方式继续拆分,直到只剩一个元素时开始返回;逐层相加,最终得到总和。调用栈负责保存每一层等待计算的状态。
def printStack(n,base):
convString = "0123456789ABCDEF"
# 查表
if n < base:
return convString[n]
# 如果这个数小于进制则直接输出
else:
return printStack(n//base,base) + convString[n % base]
# 否则输出转换数除以进制数,再加上它的余数
print(printStack(335,10))
print(printStack(335,16))
#递归进制转换
思路说明: 不断除以进制:商送进递归继续除,余数查表暂存;等除到0后,从递归栈里一层层出来,把余数字符从后往前拼起来,就是结果。
贪心算法
贪心策略是一种算法设计思想:在每一步都做出当前看来最优的选择(局部最优),期望通过一系列局部最优选择,得到全局最优解。简单来说走一步看一步,只看眼前,不看长远。
我们拿最经典的找零钱问题举例子
贪心策略:每次都拿能拿的最大面额
│
│ 36元 → 拿 25元(剩11)→ 拿 10元(剩1)→ 拿 1元
│ 结果:25 + 10 + 1 = 3枚硬币 ✅
现实生活中,收银员找钱也是这样,先把最大的面值给你凑齐,然后再凑第二大面值,再凑第三大面值
动态规划
动态规划(DP) 是一种用于解决具有重叠子问题和最优子结构性质的问题的算法设计策略。
我在学习动态规划的时候,会经常去想这个东西的每个分支是怎么实现的,实际上动态规划没那么复杂,简单来说动态规划就是:建一个表,想好第一行/第一列怎么填,找到每个格子从前面的哪些格子推出来的规律,按顺序把表填满,最后从表里拿出答案
动态规划的简单应用距离
#打家劫舍问题的动态规划算法
class Solution(object):
def rob(self, nums):
dpi = [0] * (len(nums))
dpi[0] = nums[0]
if len(nums) == 1:
return nums[0]
if nums[0] > nums[1]:
dpi[1] = nums[0]
else:
dpi[1] = nums[1]
#dpi表初始化完成
if len(nums) > 2:
for i in range(2, len(nums)):
dpi[i] = max(dpi[i-2]+nums[i], dpi[i-1])
return dpi[len(nums)-1]
s = Solution()
print(s.rob([1,7,6,9,8,5,1]))
print(s.rob([0]))
**思路说明:**首先,把要判断的价值列表传给rob函数。函数里先初始化一个DP表,全部设为0。DP表的第一个元素直接设为第一个房子的价值,因为只有一个房子时这就是最优解。如果整个列表只有一个房子,就直接返回这个值。
接下来处理第二个房子:比较第一个和第二个的价值,谁大就把谁作为DP表第二个位置的值。这样,前两个位置的初始化就完成了。
如果房子数量大于2,就开始循环填表。因为前两个已经填好了,所以从第三个位置一直填到最后一个。递推公式是:对比“不偷前一家(也就是用前前家的最优解加上当前房子的价值)”和“偷前一家(也就是直接取前一家最优解,不偷当前房子)”,谁更大,就把谁填入DP表的当前位置。就这样一直算到最后。
最后,返回DP表的最后一个值,就是能偷到的最大金额。
def dpMakeChange(coinValueList,change,MinCoins):
for cents in range(1,change+1):
#从1遍历到找零+1, 1,2,3,4,5....一直到change+1
coninCount = cents
# coninCount = 每次遍历的备份
for j in [c for c in coinValueList if c <= cents]:
# 遍历所有比找零小的面额面额
if MinCoins[cents - j] + 1 < coninCount:
# 判断“先用一枚面额为 j 的硬币,再凑剩余金额”是否比当前已知的最优方案更省硬币。
coninCount = MinCoins[cents - j] + 1
#如果是最优方案则更新最有方案
MinCoins[cents] = coninCount
#遍历完后把当前的最优解存入MinCoins
return MinCoins[change]
print(dpMakeChange([1,5,10,25],63,[0]*64))
#货币找零的动态规划代码
**思路说明:**第一,先建立一个列表MinCoins,长度是64(从0到63)。这个列表用来记录凑每个金额最少需要多少个硬币。比如MinCoins[5]就表示凑5美分的最优硬币数。
第二,外层循环从1美分一直遍历到63美分。对每一个金额cents,先假设最坏情况:全部用1美分硬币,所以硬币数coinCount等于cents本身。比如凑6美分就先假设需要6个1美分硬币。
第三,内层循环遍历所有比当前金额小的硬币面额。对每一种硬币,先用一枚这个硬币,然后剩下的金额就去MinCoins列表里查之前算好的最优解。两者相加就是这种方案的总硬币数。如果这个数比当前记录的coinCount小,就更新coinCount。
内层循环结束后,MinCoins[cents]就记录下这个金额的最优解。
最后返回MinCoins[change],就是凑63美分需要的最少硬币数。
整个过程就是从小到大填表,每个金额的解都依赖于前面更小金额的解,典型的动态规划自底向上思想。
顺序查找算法
顺序查找是最简单直接的查找算法
核心思想:从数据的第一个元素开始,按顺序(下标)逐个比较,直到找到目标值或者遍历完所有数据,如果有匹配的数据项就返回,如果没有匹配的数据项则返回查找失败

无序列表查找代码(顺序查找)
def sequentialSearch(alist,item):
pos = 0
found = False
while pos < len(alist) and found == False:
if alist[pos] == item:
found = True
else:
pos = pos + 1
return found
#无序列表查找代码(顺序查找)
对alist进行遍历,每一次都遍历,如果有则返回True,如果没有则返回Flash
有序表查找代码(顺序查找)
def orderedSequentialSearch(alist,item):
pos = 0
found = False
stop = False
while pos < len(alist) and found == False and stop == False:
if alist[pos] == item:
found = True
else:
if alist[pos] > item:
stop = True
else:
pos = pos + 1
return found
testlist = [0,1,2,6,9,13,17,19,22,25,29,33,35]
print(orderedSequentialSearch(testlist,3))
print(orderedSequentialSearch(testlist,19))
#有序列表查找代码(顺序查找)
与无序列表唯一的区别就是每次执行都会判断当前的值喝比对的值的大小,如果比当前值小的查找完了,就没必要向后继续查找了,则直接退出
顺序查找的时间复杂度
无序表
| 情况 | 最好情况 | 最坏情况 | 平均情况 |
|---|---|---|---|
| 目标元素存在 | 1 | n | n/2 |
| 目标元素不存在 | n | n | n |
有序表
| 情况 | 最好情况 | 最坏情况 | 平均情况 |
|---|---|---|---|
| 目标元素存在 | 1 | n | n/2 |
| 目标元素不存在 | 1 | n | n/2 |
二分查找算法
二分查找是一种在有序表中快速查找目标元素的算法。
它的核心思想是:每次都从中间开始找,根据中间值和目标值的大小关系,直接把搜索范围缩小一半,重复这个过程直到找到目标或者确定不存在。
二分查找法只适用于有序表!!!
二分查找代码
def binarySearch(alist,item):
first = 0
last = len(alist)-1
found = False
while first <= last and found==False:
midpoint = (first+last)//2
if alist[midpoint] == item:
found = True
else:
if item < alist[midpoint]:
last = midpoint - 1
else:
first = midpoint + 1
return found
testlist = [0,1,5,8,13,17,19,32,41]
print(binarySearch(testlist,3))
print(binarySearch(testlist,19))
先创建左边界数指向0,然后右边界数指向最后一个,然后除以二取中间数进行比对,如果正好等于则直接返回Ture,如果要匹配的值小于当前值,,则右边界指针g指向这个值的左侧,然后再取中间值进行比对,如果要匹配的值大于当前值,则左边界指针指向这个值的右侧再次匹配,直到匹配到,如果左边界和右边界错位了,则退出循环,返回False
二分查找时间复杂度
| 比较次数 | 剩余数据项的近似数量 |
|---|---|
| 1 | n/2 |
| 2 | n/4 |
| 3 | n/8 |
| … | … |
| i | n/2ⁱ |
冒泡排序
冒泡排序是一种最简单的排序算法。
它的核心思想是:重复遍历要排序的列表,每次比较相邻的两个元素,如果顺序不对就交换它们。这样每一轮遍历都会把当前未排序部分中最大(或最小)的元素“冒泡”到正确的位置
冒泡排序代码
def bubbleSort(alist):
for passnum in range(len(alist)-1,0,-1):
for i in range(passnum):
if alist[i] > alist[i+1]:
#如果左边的比右边的大,则放到右边
temp = alist[i]
alist[i] = alist[i+1]
alist[i+1] = temp
alist = [54,26,93,17,77,31,44,55,20]
bubbleSort(alist)
print(alist)
这个方法是把最大值放到左边
这个方法的时间复杂度是O( n 2 n^2 n2)。
冒泡排序的性能改进(短冒泡排序)
def shoetBubbleSort(alist):
exchangees = True
# 创建exchangess,检测这一轮比较中算法有交换
passnum = len(alist)-1
while passnum > 0 and exchangees == True:
exchangees = False
#每一大轮都会初始化exchangees为False
for i in range(passnum):
if alist[i] > alist[i+1]:
#然后逐个比较判断,如果左边比右边大则向右交换,并吧exchangess改边为True表示已经换位了
exchangees = True
temp = alist[i]
alist[i] = alist[i+1]
alist[i+1] = temp
passnum = passnum - 1
alist = [54,26,93,17,77,31,44,55,20]
shoetBubbleSort(alist)
print(alist)
#短冒泡排序:如果在某一轮比较中,一次交换都没有发生,说明列表已经是有序的了,就可以提前结束排序。
短冒泡排序就是给普通冒泡排序加了个 “提前结束” 的优化,在数据接近有序的场景下,能显著减少循环次数,效率更高;但最坏情况(数据完全逆序)下,它和普通冒泡排序的时间复杂度一样,都是 O (n²)。
选择排序
选择排序是一种简单的排序算法,核心思想是:每次从待排序的列表中找到最大的元素,放到已排序部分的末尾。
def selectionSort(alist):
for fillslot in range(len(alist)-1,0,-1):
# 初始化最大值为0
positionOfMax = 0
for location in range(1,fillslot+1):
if alist[location] > alist[positionOfMax]:
positionOfMax = location
temp = alist[fillslot]
alist[fillslot] = alist[positionOfMax]
alist[positionOfMax] = temp
alist = [54,26,93,17,77,31,44,55,20]
selectionSort(alist)
print(alist)
因为冒泡排序每次还要交换,选择排序只需要一直比较,然后保存最大值,直到最后在交换位置,这样交换的时间复杂度是o(n)比冒泡排序的最坏情况要好很多
插入排序
插入排序的核心思想是:将列表分为已排序和未排序两部分,每次从未排序部分取出一个元素,插入到已排序部分的正确位置。

如图所示,第一步去取第二个数据项插入到子列表的合适位置,第二部去取第三个数据项,和前两个数据项进行对比,然后插入到合适的位置,以此类推经过n-1次运算,即可完成所有的排序

插入排序代码
def insertionSort(alist):
for index in range(1,len(alist)):
currentvalue = alist[index]
#把当前值寄存到currentValue变量中
position = index
#保存当前要插入到子列表的下标
while position > 0 and alist[position-1] > currentvalue:
# 开始循环,当左边的值大于要插入的值,则再向左走一个进行比对,当然目前比对过的值补上之前插入值的空位
alist[position] = alist[position-1]
position = position - 1
# 如果移到最小位都没用结束,就把这个值放在最小位,当然如果左边的值比较小,就把值放在当前位置
alist[position] = currentvalue
alist = [54,26,93,17,77,31,44,55,20]
insertionSort(alist)
print(alist)
由于移动操作仅包含一次赋值,是交换操作的1/3,所以插入排序的性能比选择排序好一点
谢尔排序
谢尔排序,也叫缩量缩小排序,是插入排序的一种改进版本。
谢尔排序的核心思想是:先让列表实现“部分有序”,也就是让相隔较远的元素先排好序,最后再用一次插入排序完成全部排序。它通过引入“增量”这个概念,每次比较和移动的元素之间隔着一定的距离。

到这里可以看出,列表的顺序已经大概有序,但是不是完全有序,这时我们再对列表进行增减缩小,也就是3//2==1

然后我们最后一趟再进行一次完整的插入排序,这里因为之前已经大概排序过了,所以仅需要少量移动即可完成排序
希尔排序代码
def shellSort(alist):
#间隔设定
sublistcount = len(alist) // 2
while sublistcount > 0:
# 子列表排序
for startPosition in range(sublistcount):
gapInsertionSort(alist, startPosition, sublistcount)
print("增量为", sublistcount, "时,列表变为", alist)
# 间隔缩小
sublistcount = sublistcount // 2
def gapInsertionSort(alist, startPosition, gap):
for i in range(startPosition, gap):
currentValue = alist[i]
position = i
while position >= gap and alist[position - gap] > currentValue:
alist[position] = alist[position - gap]
position = position - gap
alist[position] = currentValue
alist = [54, 26, 93, 17, 77, 31, 44, 55, 20]
shellSort(alist)
print(alist)
增量缩小排序的时间复杂度大概在O( n 1 n^1 n1)到之O( n 2 n^2 n2)间
归并排序
归并排序是理解分治法和递归最经典的算法之一
归并排序的核心思想就两句话:
- 分:把一个大数组从中间切成两个小数组,递归地把它们排好序。
- 治:把两个已经排好序的小数组合并成一个大数组。

归并排序代码
def mergeSort(alist):
if len(alist)>1:
mid = len(alist)//2
left = alist[:mid]
right = alist[mid:]
#如果大于1,则从中间切片成为left和right两个切片
mergeSort(left)
#递归调用left
mergeSort(right)
#递归调用right
i = j = k = 0
#初始化i j k为0
while i < len(left) and j < len(right):
# 当左边和右边都有元素时,比较并合并
if left[i] < right[j]:
alist[k] = left[i]
i = i + 1
# 如果left当前元素更小,放入结果,left指针后移
else:
alist[k] = right[j]
j = j + 1
# 如果right当前元素更小或相等,放入结果,right指针后移
k = k + 1
#结果数组指针后移
while i < len(left):
alist[k] = left[i]
i = i + 1
k = k + 1
#如果right已经取完,把left剩余的所有元素依次放入结果
while j < len(right):
alist[k] = right[j]
j = j + 1
k = k + 1
#如果left已经取完,把right剩余的所有元素依次放入结果
return alist
#把结果数组返回
print(mergeSort([5,6,11,66,88,44,3,2,11,4,71]))
归并排序的时间复杂是o(n log n)
快速排序
快速排序和归并排序一样是分治法,但思路正好相反:
- 归并排序:先递归拆分,合并时排序(先分后治)
- 快速排序:先分区排序,再递归处理两边(先治后分)
核心思想
- 选一个基准值(pivot)
- 确定中值的位置,设置左右标,左边向右边移动,碰到比中值大的就停止,右标向左边移动,碰到比中值小的就停止,然后左右标所指向的数据项交换
- 然后继续移动,直到左标移动到右标的右侧,停止移动,这时右标所指位置就是中值应该所处的位置
- 将中值和这个位置交换即可完成分裂

快速排序代码
def quickSortHelper(alist, first, last):
if first < last:
#左指针和右指针之间必须有两个元素才排序,否则结束
splitpoint = partition(alist, first, last)
#获取到排序完成的列表
quickSortHelper(alist, first, splitpoint - 1)
#把排序的左边进行排序
quickSortHelper(alist, splitpoint + 1, last)
#把排序的右边进行排序
def partition(alist, first, last):
pivotvalue = alist[last] #选定最后一个值为基准值
leftmark = first # 左指针初始值为0
rightmark = last - 1 #右指针初始值倒数第二个数
done = False # #标记分区是否完成
while done == False:
#如果分区没完成则执行以下内容
while leftmark <= rightmark and alist[leftmark] <= pivotvalue:
leftmark += 1
#如果指针没用交叉 并且 左指针指向的数比基准值小则左指针向右移动,否则指针不动,指向这个比基准值大的,等右指针不动,然后交换
while rightmark >= leftmark and alist[rightmark] >= pivotvalue:
rightmark -= 1
# 如果指针没用交叉 并且 右指针指向的数比基准值小则右指针向左移动,否则指针不动,指向这个比基准值小的,等左指针不动,然后交换
if rightmark < leftmark:
done = True
#如果右指针到了左指针的左边,也就是交叉了,则完成本次分区
else:
a = alist[leftmark]
alist[leftmark] = alist[rightmark]
alist[rightmark] = a
#如果没有交叉,左右指针停止则交换值的位置
a = alist[first]
alist[first] = alist[rightmark]
alist[rightmark] = a
#这时右指针指向了中值的位置,基准值和中值位置的属性值进行互换,然后返回本次排序
return leftmark
alist = [54, 26, 93, 17, 77, 31, 44, 55, 20]
quickSortHelper(alist, 0, len(alist) - 1)
#传输数据,最后一个数据作为中值
print(alist)

835

被折叠的 条评论
为什么被折叠?



