导论
- 开发一个能够逐步解决问题的算法。
- python 是解释型语言
数据
-
内建原子数据类型
俩大内建数据 实现了 整数类型和浮点数类型。 python类就是 int 和 float
// 整除运算符。
布尔数据类型 bool. True . False
变量可以指向不同类型的数据 -
内建集合数据类型
列表、字符串、元组是有序集合,集、字典是无序集合。
列表:0个或多个python数据对象的引用的有序集合,列表是异构的。所有的数据对象可以不是同一个类。
索引 [] , 连接 + , 重复 *,成员 in,长度 len, 切片 [:]
快速初始化列表可以使用 重复运算 [0] * 6
.
提供的方法:
append(item)、insert(i, item)、pop()删除并返回列表中最后一个元素
sort():列表元素排序。
reverse:倒序
del :删除 列表中第i个位置的元素。
index(item): 返回item第一次出现的下标。
count(item): 返回item在列表中出现的次数。
remove(item): 移除第一次出现的item.
range(): list(range(10)) 从0开始, list(range(5,10))生成一个代表值序列的范围对象。使用list函数以列表形式看到范围对象的值。list(range(5,10,2)) 间隔是2.
字符串是字符的有序集合。字符串也是序列。
提供的方法:
center(w): 返回一个字符串,原字符串居中,使用空格填充新字符串,使其长度为w.
count(item): 统计item出现的次数。
ljust(w):原字符串靠左填充字符串
rjust(w): 原字符串靠右填充字符串
lower(): 字符串转小写。
upper():字符串转大写
find(item): 返回item第一次出现时的下标。
split(schar): 在schar位置将字符串分割成子串。如果没有分隔符,会寻找 制表符、换行符、空格等空白字符。
列表和字符串的区别在于:列表可以被修改,字符串则不能。
元组和列表类似,但元组也是不可修改的。
集 set是由0个或多个不可修改的Python数据对象组成的无序集合
集不允许重复元素。集是异构的。
支持的运算:
in、len、|、&、-、 <= (询问一个集是否都在另一个集中)
提供的方法:
union(otherset): 返回一个包含aset和otherset的所有元素的集。
intersection(otherset): 返回一个仅包含俩个集共有元素的集
difference(otherset): 只出现在aset中的元素
issubset(otherset): aset是否为otherset的子集
add(item): 添加一个元素
remove(item): 删除一个元素
pop():随机删除一个元素
clear(): 清除aset中的所有元素。
字典是无序结构,访问字典的语法和序列类似,只是使用键来访问。
键的位置是由散列来决定的。
运算符:[]、 in、 del
支持的方法:
keys():返回所有的键对象。
values(): 返回所有值对象
items(): 返回所有的键值对象
get(k): 返回k对应的值,没有返回None.
get(k, alt): 返回k对应的值,没有则返回alt.
-
异常处理
俩种错误:语法错误、逻辑错误。
逻辑错误导致运行时异常,进而导致程序终止运行。
运行时错误被称为异常。 -
类
构造方法来创建对象,self形参必须是第一个参数,
__str__方法 __add__方法。
浅相等:同一个对象的俩个引用
深相等:判断对象的值相等。__eq__方法。 -
继承
子类可以从父类继承特征数据和行为。 父类称为超类。
子类和父类是 IS-A 的关系。是一个的关系。
算法分析
何为算法分析
计算资源: 占用的内存,
时间:执行的时间
独立于程序和计算机的指标。
大O记法
Order of magnitude 数量级。 问题规模 :n
有时候不仅依赖于问题规模,还依赖与数据值,所以还要用最好情况、最坏情况、平均情况来描述性能。
常见的大O函数
1,logn,n,nlogn线性对数,n2,n3,2^n指数
异词序检测示例
如果一个字符串只是重排了另一个字符串的字符,那么这个字符就是另一个的异序词。
简化问题:俩个字符串长度相同
- 方案1:清点法
清点第一个字符串的每个字符,看它们是否都出现在第二个字符串。
如果是,必然是异序词。
def anagramSolution(s1, s2): alist = list(s2) pos1 = 0 still_ok = True while pos1 < len(s1) and still_ok: pos2 = 0 found = False while pos2 < len(alist) and not found: if s1[pos1] == alist[pos2]: found = True else: pos2 += 1 if found: alist[pos2] = None else: still_ok = False return still_ok
算法分析:对于s1中的n个字符,都要遍历s2中的n个字符。
从1到n的整数之和,复杂度是O(n^2)
- 方案2:排序法
按照字母表顺序排序,异序词得到的结果是同一个字符串。
def anagramSolution2(s1, s2): alist1 = list(s1) alist2 = list(s2) alist1.sort() alist2.sort() pos = 0 matches = True while pos < len(s1) and matches: if alist1[pos] == alist2[pos]: pos += 1 else: matches = False return matches
算法分析:排序的时间复杂度是O(n^2) 或 O(nlogn)。
- 方案3:蛮力法
pass - 方案4:计数法
俩个异序词有同样数目的a、同样数目的b、同样数目的c等等等。。
要判断俩个字符串是否为异序词,先数一下每个字符出现的次数。
字符有26中,需要使用26个计数器。
俩个计数器列表相等,那么就是异序词。
def anagramSolution3(s1, s2): c1 = [0] * 26 c2 = [0] * 26 for i in range(len(s1)): pos = ord(s1[i]) - ord('a') c1[pos] += 1 for i in range(len(s2)): pos = ord(s2[i]) - ord('a') c2[pos] += 1 j = 0 still_ok = True while j < 26 and still_ok: if c1[j] == c2[j]: j += 1 else: still_ok = False return still_ok
时间复杂度:O(n);
Python数据结构的性能
列表
生成列表的四种方式
for循环+,追加 append ,列表解析式,range.
def test1(): l = [] for i in range(1000): l = l + [i] def test2(): l = [] for i in range(1000): l.append(i) def test3(): l = [i for i in range(1000)] def test4(): l = list(range(1000)) t1 = Timer("test1()", "from __main__ import test1") print(t1.timeit(number=1000)) t1 = Timer("test2()", "from __main__ import test2") print(t1.timeit(number=1000)) t1 = Timer("test3()", "from __main__ import test3") print(t1.timeit(number=1000)) t1 = Timer("test4()", "from __main__ import test4") print(t1.timeit(number=1000))
0.859432
0.0531064
0.030979700000000054
0.0088275000000001
range最快。
基本数据结构
何为线性数据结构
元素与元素之间是一对一的, 栈、队列、链表、列表
非线性:树、图。
栈
LIFO Last In First Out
栈抽象数据类型
- Stack() 创建一个空栈
- push(item)
- pop() 删除栈顶端元素,返回顶端的元素。
- peek() 返回栈顶端的元素。不移除该元素。
- isEmpty(): 检查栈是否为空
- size(): 栈中元素的数目
python是没有原生的栈。
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[len(self.items) - 1] def size(self): return len(self.items)
用栈来匹配括号
能够分辨括号匹配正确与否,
从左到右处理括号时:最右边的无匹配的左括号必须与接下来的第一个右括号相匹配。
算法执行逻辑:
从一个空栈开始,从左到右依次处理括号,
如果遇到左括号,就入栈,需要一个右括号。
如果遇到右括号,就调用pop操作。只要栈中有左括号都能有与之匹配,那么整个括号串就是匹配的。
处理完之后,栈应该是空的。
def parChecker(symbolString): s = Stack() balanced = True index = 0 while index < len(symbolString) and balanced: symbol = symbolString[index] if symbol == "(": s.push(symbol) else: if s.isEmpty(): balanced = False else: s.pop() index += 1 if balanced and s.isEmpty(): return True else: return False
符号匹配
def matches(open, close): opens = '([{' closers = ')]}' return opens.index(open) == closers.index(close) def parChecker(symbolString): s = Stack() balanced = True index = 0 while index < len(symbolString) and balanced: symbol = symbolString[index] if symbol in "([{": s.push(symbol) else: if s.isEmpty(): balanced = False else: top = s.pop() if not matches(top, symbol): balanced = False index += 1 if balanced and s.isEmpty(): return True else: return False
将十进制树转换成二进制数
除以2 的算法。
假设待处理的整数大于0. 用一个简单的循环不停地将十进制数除以2.并且记录余数。
第一次除以2的结果能够用于区分偶数和奇数。
如果是偶数,余数为0. 如果是奇数,余数为1,。
def divideBy2(decNumber): remstack = Stack() while decNumber > 0: rem = decNumber % 2 remstack.push(rem) decNumber = decNumber // 2 binString = "" while not remstack.isEmpty(): binString = binString + str(remstack.pop()) return binString
进阶:十进制数转换成任意进制数。
def divideByN(decNumber, base): digits = "0123456789ABCDEF" remstack = Stack() while decNumber > 0: rem = decNumber % base remstack.push(rem) decNumber = decNumber // base binString = "" while not remstack.isEmpty(): binString = binString + digits[remstack.pop()] return binString
前序、中序、后序表达式
根据运算符的位置来区分
B * C 中序表达式
对于 A + B * C ,有运算优先级,咱人类可以知道,但是计算机不知道。
计算机需要明确地直到以何种顺序进行何种运算。
一种杜绝歧义的写法是 完全括号表达式。(A + (B * C)),每一个运算符都添加一对括号。没有任何歧义。
通过改变运算符与操作树的相对位置。 +AB 前序表达式, AB+ 后序表达式
中序表达式: A + B, A + B * C , (A + B) * C
前序表达式: + A B, + A * B C , * + A B C
后序表达式: AB + , A B C * + , A B + C *
只有中序表达式需要括号来消除歧义。前序和后序表达式的运算顺序完全由运算符的位置决定。
- 从中序向前序和后序转换
A + B * C -> ( A + ( B * C))
每一对括号对应着一个中序表达式。
如果将乘号 移到右括号所在的位置, 并且去掉左括号。就是得到 (A + BC ) ,转换成了对应的后序表达式,最终结果: ABC+
如果将乘号 移到左括号所在的位置, 并且去掉右括号。就是得到 (A + * BC),转换成了对应的前序表达式, 最终结果:+A*BC
步骤就是:先降任意复杂的中序表达式转换成 完全括号表达式,然后将括号内的运算符移动到左括号(前序)或者 右括号(后序)
- 从中序到后序的通用转换法
将任意中序表达式转换成后序表达式的算法。
A + B * C > A B C * +
操作数的相对位置保持不变。只有运算符改变了位置。
中序表达式中第一个出现的运算符是+。
后序表达式中,由于*的优先级更高。在本例中,中序表达式的运算符顺序和后序表达式的相反。
从左向右遍历转换,由于运算符右边的操作数还未出现,因此需要将运算符保存在某处, 同时由于运算符有不同的优先级,因此可能需要反转它们的保存顺序。
鉴于这种反转特性,使用栈来保存运算符就十分合适。
对于 (A + B) * C
> AB+C*
这种情况呢?
括号改变了运算符的优先级,
所以:遇到左括号是,需要将其保存,表示接下来会遇到高优先级的运算符,需要等到对应的右括号才能确定其位置。
当右括号出现,便可以将运算符从栈中取出来。
遍历的时候,利用栈来保存运算符,提供反转特性,栈的顶端永远是最新添加的运算符。
每新添加一个运算符时,都需要对比它与栈中运算符的优先级。
1) 栈 opstack:保存运算符, 列表:保存结果。
2) 使用字符串方法split 将输入的中序表达式转换成一个列表
3) 从左往右扫描列表:
- 如果标记是操作数,将其添加到结果列表的末尾
- 如果标记是左括号,压入opstack栈中
- 如果是右括号,反复从openstack栈中移除元素,直到移除对应的左括号,将从栈中取出的每一个运算符都添加到结果列表的末尾。
- 如果标记是运算符,将其压入openstak栈中,需要先从栈中取出优先级更高或相同的运算符,并将它们添加到结果列表末尾。
4)当处理完输入表达式后,检查openstack,将其中残留的所有运算符 全部添加到结果列表的末尾。
def infixToPostfix(infixexpr): prec = {} prec["*"] = 3 prec['/'] = 3 prec['+'] = 2 prec['-'] = 2 prec['('] = 1 opStack = Stack() postfixList = [] tokenList = infixexpr.split() print("tokenList:", tokenList) for token in tokenList: if token in string.ascii_uppercase: postfixList.append(token) elif token == '(': opStack.push(token) elif token == ')': topToken = opStack.pop() while topToken != '(': postfixList.append(topToken) topToken = opStack.pop() else: # print(opStack.peek()) # print(prec[token]) while (not opStack.isEmpty()) and (prec[opStack.peek()] >= prec[token]): postfixList.append(opStack.pop()) opStack.push(token) while not opStack.isEmpty(): postfixList.append(opStack.pop()) return " ".join(postfixList) print(infixToPostfix("( A + B ) * ( C + D )"))
- 计算后序表达式
456+
栈需要保存操作数,而不是运算符。
先456入栈,遇到操作符,意味着将最近俩个数相乘。通过执行俩次出栈。进行运算之后,然后将结果入栈。
处理完之后,栈中只剩一个数,这个数就是结果。
def posfixEval(postfixExpr): operandStack = Stack() tokenList = postfixExpr.split() for token in tokenList: if token in '0123456789': operandStack.push(token) else: operand2 = operandStack.pop() operand1 = operandStack.pop() result = doMath(token, int(operand1), int(operand2)) operandStack.push(result) return operandStack.pop() def doMath(op, op1, op2): if op == "*": return op1 * op2 elif op == "/": return op1 / op2 elif op == "+": return op1 + op2 else: return op1 - op2 print(posfixEval('7 8 + 3 2 + /'))
队列
添加操作发生在尾部,移除操作发生在头部,FIFO.
队列抽象数据类型
- Queue() 创建一个空队列,
- enqueue(item) 队列的尾部添加一个元素。
- dequeue() 头部移除一个元素。
- isEmpty()
- size() 元素的数目。
class Queue: def __init__(self): self.items = [] def isEmpty(self): return self.items == [] def enqueue(self, item): self.items.insert(0, item) def dequeue(self): return self.items.pop() def size(self): return len(self.items)
传土豆
def hotPotato(namelist, num): simqueue = Queue() for name in namelist: simqueue.enqueue(name) while simqueue.size() > 1: for i in range(num): simqueue.enqueue(simqueue.dequeue()) simqueue.dequeue() return simqueue.dequeue()
打印任务
class Printer: '''打印机''' def __init__(self, ppm): self.pagerate = ppm # 打印速度,每分钟打印多少页 self.currentTask = None # 当前打印任务 self.timeRemaining = 0 # 当前任务剩余时间 def tick(self): '''减量计时,''' if self.currentTask != None: 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, time): self.timestamp = 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) if num == 180: return True else: return False def simulation(numSeconds, pagePerMinute): labprinter = Printer(pagePerMinute) printQueue = Queue() 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() # 打印机执行任务 averageWait = sum(waitingtimes) / len(waitingtimes) print("Average Wait %6.2f secs %3d task remaining." % (averageWait, printQueue.size())) for i in range(10): simulation(3600, 5)
执行结果
双端队列
前端 后端都可以添加删除元素。
某种意义上来说:双端队列是栈和队列的结合。
class Queue: def __init__(self): self.items = [] def isEmpty(self): return self.items == [] def addFront(self, item): self.items.append(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)
回文检测器
回文:从前往后读 和 从后往前读 都一样的字符串。 radar \ toot \
def palchecker(aString): chardeque = Queue() for ch in aString: chardeque.addRear(ch) stillEqual = True while chardeque.size() > 1 and stillEqual: first = chardeque.removeFront() last = chardeque.removeRear() if first != last: stillEqual = False return stillEqual print(palchecker('lsdkjsfdkf')) print(palchecker('toot')) print(palchecker('tofot'))
列表
列表是元素的集合,
实现无序列表 : 链表
无序列表需要维持元素之间的相对位置。并不需要在连续的内存空间中维护这些位置信息。
- Node 类
class Node: def __init__(self, initdata): self.data = initdata self.next = None def getData(self): return self.data def getNext(self): return self.next def setData(self, newdata): self.data = newdata def setNext(self, newnext): self.next = newnext
- UnorderedList 类
无序列表是基于节点集合构建的。只需要知道第一个节点。 必须包含指向第一个节点的引用。
class UnorderedList: def __init__(self): self.head = None def isEmpty(self): return self.head is None def add(self, item): temp = Node(item) temp.setNext(self.head) self.head = temp def length(self): current = self.head count = 0 while current is not None: count += 1 current = current.getNext() return count def search(self, item): current = self.head found = False while not found: if current.getData() == item: found = True else: current = current.getNext() return found def remove(self, item): current = self.head previous = None found = False while not found: if current.getData() == item: found = True else: previous = current current = current.getNext() if previous == None: # 特殊情况,移除的正好是列表的第一个元素。 self.head = current.getNext() else: # 指向下一个。 previous.setNext(current.getNext())
- OrderedList:
有序列表,就add 和 search 方法不一样
修改链表 头部是特殊的操作。
class OrderedList: def __init__(self): self.head = None def isEmpty(self): return self.head is None def length(self): current = self.head count = 0 while current is not None: count += 1 current = current.getNext() return count def remove(self, item): current = self.head previous = None found = False while not found: if current.getData() == item: found = True else: previous = current current = current.getNext() if previous == None: # 特殊情况,移除的正好是列表的第一个元素。 self.head = current.getNext() else: # 指向下一个。 previous.setNext(current.getNext()) def search(self, item): current = self.head found = False stop = False while current != None and not found and not stop: if current.getData() == item: found = True else: if current.getData() > item: stop = True else: current = current.getNext() return found def add(self, item): current = self.head previous = None stop = False # 啥时候找到位置了就停下 while current != None and not stop: if current.getData() > item: stop = True else: previous = current current = current.getNext() temp = Node(item) if previous == None: # 要插入的位置是头节点 temp.setNext(self.head) self.head = temp else: temp.setNext(current) previous.setNext(temp)
递归
将问题不断地分成更小的子问题
计算-列数之和
def listsum(numList): if len(numList) == 1: # 基本情况 return numList[0] else: # 往基本情况 靠近 return numList[0] + listsum(numList[1:])
递归三大原则
1)必须有基本情况
2)必须改变其状态向基本情况靠近
3)自己调用自己
将整数转换成任意进制的字符串
def toStr(n, base): convertString = "0123456789ABCDEF" if n < base: return convertString[n] else: return toStr(n//base, base) + convertString[n % base]
递归可视化
递归的绘制螺旋线
from turtle import * myTurtle = Turtle() myWin = myTurtle.getscreen() def drawSpiral(myTurtle, lineLen): if lineLen > 0: myTurtle.forward(lineLen) myTurtle.right(90) drawSpiral(myTurtle, lineLen - 5) drawSpiral(myTurtle, 100) myWin.exitonclick()
绘制分形树
def tree(branchLen, t): if branchLen > 5: t.forward(branchLen) t.right(20) tree(branchLen - 15, t) t.left(40) tree(branchLen - 10, t) t.right(20) t.backward(branchLen) tree(110, myTurtle)
汉诺塔
fromPole withPole toPole
借助一根中间柱子,将高度为height的一叠盘子从起点柱子 移到 到终点 柱子
1) 借助终点柱子,将高度为 height -1 的一叠盘子从起点柱子移动到 中间柱子。 fromPole ( height -1 ) > withPole
2) 将 起点柱子最大的盘移动到 终点柱子。。 fromPole (最大) > toPole
3) 借助起点柱子,将 height-1的一叠盘子从 中间柱子移动到 终点柱子。
def moveTower(height, fromPole, toPole, withPole): """ fromPloe: 起点柱子 toPole: 目的柱子 withPole: 借助柱子 """ if height >= 1: # 借助目的柱子,将height-1的盘子移到中间柱子 # 目的柱子是中间柱子了,起始的三个柱子的角色有变换 # 记住movaTower, 4个形参的实际作用。 moveTower(height - 1, fromPole, withPole, toPole) # 将起点柱子的最后最大一个盘子移动到目的柱子 moveDisk(fromPole, toPole) # 现在 只需要把中间柱子的height-1个盘子移动到 目的柱子就好 # 借助柱子是 起点柱子了,起点柱子是中间柱子了,目的主机还是目的柱子。 moveTower(height - 1, withPole, toPole, fromPole) count = 0 def moveDisk(fp, tp): global count count += 1 print(f'{count} : moving disk from {fp} to {tp}') moveTower(5, 'A', 'B', 'C')
探索迷宫
def searchFrom(maze, startRow, startCloumn): maze.updatePosition(startRow, startCloumn) # 1. 遇到墙 if maze[startRow][startCloumn] == OBSTACLE: return False # 2. 已经走过 if maze[startRow][startCloumn] == TRIED: return False # 3. 找到出口,0 行 0 列,最后一行 ,最后一列。 if maze.isExit(startRow, startCloumn): maze.updatePosition(startRow, startCloumn, PART_OF_PATH) return True # 标记走过这个格子 maze.updatePosition(startRow, startCloumn, TRIED) # 4. 尝试向四个方向移动 found = searchFrom(maze, startRow - 1, startCloumn) or \ searchFrom(maze, startRow + 1, startCloumn) or \ searchFrom(maze, startRow, startCloumn - 1) or \ searchFrom(maze, startRow, startCloumn + 1) # 如果找到了。 if found: maze.updatePosition(startRow, startCloumn, PART_OF_PATH) else: maze.updatePosition(startRow, startCloumn, DEAD_END) return found
动态规划
优化某些值
比如:俩点之间最短路径,最佳拟合线,最小对象集合。
解决优化问题,一个策略是动态策略
经典例子:找零时使用最少的硬币。
一个顾客使用一张一美元的纸币购买了价值37美分的物品,最少需要找给该顾客多少硬币
贪婪算法:从面值最大的硬币开始,使用尽可能多的硬币。然后尽可能使用第二大的硬币。
但是如果有21分的面值,那么最优解是3个21分。贪婪算法就是错的。
一种必定能得到最优解的方法: 递归, 动态规划一般都可以用递归优化得来。一般是通过添加记忆功能。递归一般都有重复的计算,减少重复的计算就快了。
基本情况:如果要找的零钱与硬币的面值相同,那么只需找一枚硬币即可。
如果要找的零钱与硬币的面值不同:多种选择:
1)1枚1分的硬币+找零金额减去1分之后所需的硬币
2)1枚5分的硬币+找零金额减去5分之后所需的硬币
等等等。
def recMC(coinValueList, change): minCoins = change # 初始化最少用的硬币,最差情况,change个1分硬币 if change in coinValueList: return 1 else: for i in [c for c in coinValueList if c <= change]: numCoins = 1 + recMC(coinValueList, change - i) # 递归调用 if numCoins < minCoins: # 如果当前所需硬币少于minCoins minCoins = numCoins # 更新最小值。 return minCoins print(recMC([1, 5, 10, 25], 63)) print(recMC([1, 5, 10, 21, 25], 63))
运行了好久。。。
减少计算量的关键在于记住已有的结果。
简单的做法就是把最少硬币数的计算结果存储在一张表中。在计算之前,检查结果是否已在表中。如果是,就直接使用结果。
def recMC(coinValueList, change, knowResults): minCoins = change # 初始化最少用的硬币,最差情况,change个1分硬币 if change in coinValueList: knowResults[change] = 1 return 1 elif knowResults[change] > 0 : return knowResults[change] # 如果已经计算过,直接返回。 else: for i in [c for c in coinValueList if c <= change]: numCoins = 1 + recMC(coinValueList, change - i, knowResults) # 递归调用 if numCoins < minCoins: # 如果当前所需硬币少于minCoins minCoins = numCoins # 更新最小值。 knowResults[change] = minCoins # 记录最小值 return minCoins print(recMC([1, 5, 10, 25], 63, [0] * 64)) print(recMC([1, 5, 10, 21, 25], 63, [0] * 64))
真正的动态规划算法会用更系统的方法来解决问题。
从1分找零开始,然后系统地一直计算到所需的找零金额。这样做可以保证在每一步都已经知道任何小于当前值得找零金额所需的最少硬币数。
def dpMakeChange(coinValueList, change, minCoins): for cents in range(change+1): coinCount = cents # 当前需要计算的金额。 for j in [c for c in coinValueList if c <= cents]: if minCoins[cents-j] + 1 < coinCount: # 比如当前是11分, j 的值可能是 1,5,10。有三种可能,面值已经大于余额的直接不需要算。 # 1) minConits[11 - 1] + 1 , minCoints[10] 肯定已经算出来了。然后就从这三个可能中找。一定能够找到。 # 2) minCoints[11 - 5] + 1 # 2) minCoints[11 - 10] + 1 coinCount = minCoins[cents - j] + 1 minCoins[cents] = coinCount return minCoins[change] print(dpMakeChange([1,5,10,25], 63, [0] * 64))
只是返回了最少硬币数,没有记录所用的硬币,并不能帮助我们进行实际的找零工作。
def dpMakeChange(coinValueList, change, minCoins, coinsUsed): for cents in range(change+1): coinCount = cents # 当前需要计算的金额。 newCoin = 1 # 这次新增使用的硬币。 for j in [c for c in coinValueList if c <= cents]: if minCoins[cents-j] + 1 < coinCount: # 比如当前是11分, j 的值可能是 1,5,10。有三种可能,面值已经大于余额的直接不需要算。 # 1) minConits[11 - 1] + 1 , minCoints[10] 肯定已经算出来了。然后就从这三个可能中找。一定能够找到。 # 2) minCoints[11 - 5] + 1 # 2) minCoints[11 - 10] + 1 coinCount = minCoins[cents - j] + 1 newCoin = j # 这次使用的硬币面值。当前循环肯定是添加了某种面值的硬币。 minCoins[cents] = coinCount coinsUsed[cents] = newCoin return minCoins[change] def printCoins(coinsUsed, change): coin = change while coin > 0: thisCoin = coinsUsed[coin] print(thisCoin) coin = coin - thisCoin # 更新剩余金额 # print(dpMakeChange([1,5,10,25], 63, [0] * 64), [0] * 64) coinduser = [0] * 64 print(dpMakeChange([1,5,10,25], 63, [0] * 64, coinduser)) print(coinduser) printCoins(coinduser, 63)
结果如下图,打印流程,显示coinduser[63] = 1 ,然后是coinduser[62] = 1, coniduser[61] = 1,
coinduser[60] = 10, coinduser[50] = 25, coinduser[25] = 10 . 余额为0 了,循环终止。
PS C:\project\python_ss> & C:/develope/anaconda/python.exe c:/project/python_ss/dynamic.py 6 [1, 1, 1, 1, 1, 5, 1, 1, 1, 1, 10, 1, 1, 1, 1, 5, 1, 1, 1, 1, 10, 1, 1, 1, 1, 25, 1, 1, 1, 1, 5, 1, 1, 1, 1, 10, 1, 1, 1, 1, 5, 1, 1, 1, 1, 10, 1, 1, 1, 1, 25, 1, 1, 1, 1, 5, 1, 1, 1, 1, 10, 1, 1, 1] 1 1 1 10 25 25
搜索和排序
搜索:从元素结合中找到某个特定元素的算法过程。
顺序搜索
存储与列表等集合中的数据项彼此存在线性或顺序的关系
- 无序列表的顺序搜索
def sequentialSearch(alist, item): pos = 0 found = False while pos < len(alist) and not found: if alist[pos] == item: found = True else: pos += 1 return found
因为元素是无序的,每个元素必须比较。
- 有序列表的顺序搜索
def orderedSequentialSearch(alist, item): pos = 0 found = False stop = False while pos < len(alist) and not found and not stop: if alist[pos] == item: found = True elif alist[pos] > item: stop = True else: pos += 1 return found
进一步利用有序的条件。二分搜索
def binarySearch(alist, item): first = 0 last = len(alist) - 1 found = False while first <= last and not found: midpoint = (first + last) // 2 if alist[midpoint] == item: found = True else: if item < alist[midpoint]: last = midpoint - 1 else: first = midpoint + 1 return found
散列
通过散列可以构建一个时间复杂度为O(1)的数据结构
散列表是元素的结合。散列表中的每个位置通常被称为 槽。
散列函数将散列表中的元素与其所属位置对应起来。
散列函数返回一个介于0和m-1之间的整数。有时也叫取余函数。
占用率被称为载荷因子。
散列函数遇到冲突需要解决。
- 散列函数
给定一个元素集合,能够将每个元素映射到不同的槽,这种散列函数称作完美散列函数。
元素已知,集合不变,有可能。
一般不完美。但性能已经不错了。
目标:冲突数最少,计算方便,元素均匀分布于散列表中。
折叠法:先将元素切成等长的部分,最后一部分的长度可能不同。然后将这些部分相加,得到散列值。
比如电话号码:436-555-4601. 切成 43、65、55、46、01. 相加得到210. 假设有11个槽, 210 % 11 = 1
所以被映射到1号槽。
平方取中法:将元素取平方,然后提取中间几位数,然后取余。
散列函数一定要高效,不然存储和搜索的时候负担太大了。
- 处理冲突
当俩个元素被分配到同一个槽中。必须通过一种系统化的方法在散列表中安置第二个元素。
一种方法是在散列表中找到另一个槽。比如从冲突的槽开始,顺序遍历散列表,直到找到一个空槽。将开放定址法。
线性探测。
寻找元素的时候,当散列之后的槽为不是要找的元素,因为可能有冲突,需要从当前槽开始顺序搜索,直到找到对应的元素或者空槽。
线性探测有个缺点,会使散列表中的元素出现聚集线程。一个槽发生太多冲突,线性探测会填满其附近的槽。会影响到后续插入的元素。
另一种方法是 扩展线性探测,不顺序查找空槽,而是跳过一些槽。这样能是引起冲突的元素分布的更均匀。
再散列泛指反射冲突之后再寻找另一个槽的过程。
另一种处理冲突的方法是让每个槽有一个指向元素集合 或 链表的引用。链接法 允许散列表中的同一个位置上存在多个元素。
- 实现映射抽象数据类型
键和值是一一对应的关系。
class HashTable: def __init__(self): self.size = 11 self.slots = [None] * self.size self.data = [None] * self.size def put(self, key, data): # 计算散列值。就是槽位号。 hashvalue = self.hashfunction(key, len(self.slots)) if self.slots[hashvalue] == None: # 没冲突,直接存入 self.slots[hashvalue] = key self.data[hashvalue] = data else: # 冲突了 if self.slots[hashvalue] == key: # 和自己冲突了,就是更新数据。 self.data[hashvalue] = data else: # 再散列 nextslot = self.rehash(hashvalue, len(self.slots)) while self.slots[nextslot] != None and self.slots[nextslot] != key: # 直到空槽和 找到自己key的位置 nextslot = self.rehash(nextslot, len(self.slots)) # 继续散列 if self.slots[nextslot] == None: # 找到空槽 self.slots[nextslot] = key self.data[nextslot] = data else: # 找到自己了 self.slots[nextslot] = data # 更新数据 def hashfunction(self, key, size): return key % size def rehash(self, oldhash, size): return (oldhash + 1) % size def get(self, key): startslot = self.hashfunction(key,len(self.slots)) data = None stop = False found = False position = startslot # 如果 遇到空槽就 停了。 while self.slots[position] != None and not found and not stop: if self.slots[position] == key: found = True data = self.data[position] else: position = self.rehash(position, len(self.slots)) if position == startslot: # 回到最初的起点了。没找到。就该停止了 stop = True return data def __getitem__(self, key): return self.get(key) def __setitem__(self, key, data): return self.put(key,data)
排序
冒泡排序
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
短冒泡。如果在一轮遍历中没有发生元素交换,就可以确定列表已经有序。可以提前终止。
对于只需要遍历几次的列表,冒泡排序有优势。
def bubblesortSort(alist): exchanges = True passnum = len(alist) -1 while passnum > 0 and exchanges: exchanges = False for i in range(passnum): if alist[i] > alist[i+1]: exchanges = True temp = alist[i] alist[i] = alist[i+1] alist[i+1] = temp passnum -= 1
选择排序
冒泡排序是俩俩比较 慢慢冒泡。
选择排序是稍微优化了一下,每次遍历只做依次交换,每次选择当前剩余元素最大的。
def selectionSort(alist): for fillslot in range(len(alist) - 1, 0, -1): 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
插入排序
def insertionSort(alist): for index in range(1, len(alist)): currentvalue = alist[index] # 保存当前要插入的元素。 position = index # 到了位置0,或者前面一个元素比当前元素小了。 while position > 0 and alist[position-1] > currentvalue: alist[position] = alist[position - 1] # 将前一个较大的元素后移。 position -= 1 alist[position] = currentvalue # 将插入的元素放到插入的位置。
希尔排序
对插入排序做了改进,也叫递减增量排序。
将列表分成数个子列表,并对每一个子列表应用插入排序。
如何切分列表是希尔排序的关键。 并不是连续切分。而是使用增量i(步长)选取所有间隔为i的元素组成的子列表。
景观列表仍然不算完全有序,但是通过给子列表拍戏,元素离它们的最终位置更近了。
def shellSort(alist): sublistcount = len(alist) // 2 while sublistcount > 0 : for startposition in range(sublistcount): gapInsertionSort(alist, startposition, sublistcount) print("After increments os size", sublistcount, "The list is ", alist) sublistcount = sublistcount // 2 def gapInsertionSort(alist, start, gap): for i in range(start+gap, len(alist), 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
归并排序
使用分治策略改进排序算法。
如果列表为空或只有一个元素,就是有序。的
如果列表不止一个元素,就将列表一分为2.
分俩步: 拆分 。 归并
def mergeSort(alist): print("Splitting ", alist) if len(alist) > 1 : mid = len(alist) // 2 lefthalf = alist[:mid] # 额外的空间 righthalf = alist[mid:] # 开始递归调用 mergeSort(lefthalf) mergeSort(righthalf) i = 0 # lefthalf 索引 j = 0 # righthalf 索引 k = 0 # 归并的索引 while i < len(lefthalf) and j < len(righthalf): if lefthalf[i] < righthalf[j]: alist[k] = lefthalf[i] i += 1 else: alist[k] = righthalf[j] j += 1 k += 1 while i < len(lefthalf): alist[k] = lefthalf[i] k += 1 i += 1 while j < len(righthalf): alist[k] = righthalf[j] k += 1 j += 1 print("merging ", alist)
需要额外的空间。 时间复杂度 O(nlogn)
快速排序
和归并排序一样,采用分治策略。但是不使用额外的存储空间
找一个基准值,将左边和右边的元素放到正确的一边。
def quickSort(alist): quickSortHelper(alist, 0, len(alist) - 1) def quickSortHelper(alist, first, last): if first < last: splitpoint = partion(alist, first, last) quickSortHelper(alist, first, splitpoint - 1) quickSortHelper(alist, splitpoint + 1, last) def partion(alist, first, last): pivotvalue = alist[first] leftmark = first + 1 rightmark = last done = False while not done: while leftmark <= rightmark and alist[leftmark] <= pivotvalue: leftmark += 1 while alist[rightmark] >= pivotvalue and rightmark >= leftmark: rightmark = rightmark - 1 if rightmark < leftmark: done = True else: temp = alist[leftmark] alist[leftmark] = alist[rightmark] alist[rightmark] = temp temp = alist[first] alist[first] = alist[rightmark] alist[rightmark] = temp return rightmark
树
二叉树
每个节点最多有俩个子节点, 这样的树称为二叉树
实现树的关键在于选择一个好的内部存储技巧。
Python提供俩种: 列表之列表,节点与引用
列表之列表
根节点是列表第一个元素,第二个元素是左子树的列表。第三个元素是右子树的列表。
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, [], []]) 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 def insertLeft(self, newNode): if self.leftChild == None: self.leftChild = BinaryTree(newNode) else: t = BinaryTree(newNode) t.leftChild = self.leftChild self.rightChild = 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.rightChild def setRootVal(self, obj): self.key = obj def getRootVal(self): return self.key
解析树
- 如何根据完全括号表达式构建解析树
- 如何计算解析树中的表达式
- 如何将解析树还原成最初的数学表达式
第一步:将表达式字符串拆分成标记列表。
需要考虑4种标记:左括号、右括号、运算符、操作数
左括号代表新表达式的起点。 右括号代表该表达式的终点。操作数既是叶子节点。也是运算符的子节点。每个运算符都有左右子节点
定义以下四条规则:
1)如果当前是(,就为当前节点添加一个左子节点。并下沉至该子节点。
2)如果当前标记在列表 +-/* 中,就将当前节点的值设为当前标记对应的运算符。并添加右子节点。下沉到该右子节点。
3)如果当前标记是数字,将当前节点的值设为这个数,并返回至父节点
4)如果当前标记是), 就跳到当前节点的父节点。
表达式: ( 3 + ( 4 * 5 ))
标记列表: alist = ['(', '3', '+', '(', '4', '*', '5', ')',')']
构建解析树的过程中,需要追踪当前节点及其父节点。可以通过getLeftChild、getRightChild获取子节点。使用栈记录父节点。
每当要下沉至当前节点的子节点时,先将当前节点压到栈中。
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(eval(i)) parent = pStack.pop() currentTree = parent elif i in '+-*/': # 如果当前标记在列表 +-/* 中,就将当前节点的值设为当前标记对应的运算符。并添加右子节点。下沉到该右子节点。 currentTree.setRootVal(i) currentTree.insertRight('') pStack.push(currentTree) currentTree = currentTree.getRightChild() elif i == ')': # 如果当前标记是), 就跳到当前节点的父节点。 currentTree = pStack.pop() else: raise ValueError('Unknown Operator: ' + i) return eTree
有了解析树之后,计算解析树。
递归计算每颗子树得到整颗解析树的结果。
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.getRootVal()) preOrder(tree.getLeftChild()) preOrder(tree.getRightChild()) def postOrder(tree): if tree != None: postOrder(tree.getLeftChild()) postOrder(tree.getRightChild()) print(tree.getRootVal()) def inorder(tree): if tree != None: inorder(tree.getLeftChild()) print(tree.getRootVal()) inorder(tree.getRightChild())
后序遍历一个常见的用途是计算解析树,先计算左子树,再计算右子树,最后通过根节点的运算符的函数调用将俩个结果结合起来。
def postordereval(tree): opers = { '+': operator.add, '-': operator.sub, '*': operator.mul, '/': operator.truediv } res1 = None res2 = None if tree: res1 = postordereval(tree.getLeftChild()) res2 = postordereval(tree.getRightChild()) if res1 and res2: return opers[tree.getRootVal()](res1, res2) else: return tree.getRootVal()
通过中序遍历遍历解析树,可以还原不带括号的表达式。
def printexp(tree): sVal = '' if tree: sVal = '(' + printexp(tree.getLeftChild()) sVal = sVal + str(tree.getRootVal()) sVal = sVal + printexp(tree.getRightChild()) + ')' return sVal
利用二叉堆实现优先级队列
队列有一个重要的变体,叫做优先级队列。从头部移除元素,但是优先级最高的元素在前。
当一个元素入队时,可能直接被移到优先级队列的头部。
实现优先级队列的经典方法是使用叫做二叉堆的数据结构。 二叉堆的入队操作和出队操作均可达到O(logn)
二叉堆分为最小堆和最大堆(最大的元素一直在队首)
二叉堆的实现
为了使二叉堆高效的工作,利用树的对数性质来表示它。
为了保证对数性能,必须维持树的平衡。
平衡的二叉树是指:其根节点的左右子树含有数量大致相等的节点。
在实现二叉堆时,创建一颗完全二叉树来维持树的平衡。
完全二叉树:除了最底层,其他每一层的节点都是满的。在最底层,从左到右填充节点。
完全二叉树可以用一个列表来表示,不需要采用列表之列表。
- 结构属性
左子节点:2p
右子节点:2p+1
父节点:// (整除) - 堆的有序性
堆中任意元素x 及其父元素 p, p都不大于x. - 堆操作
列表的currentSize记录当前堆的大小。
插入元素。先插入到最后,然后递归和自己的父元素比较,保证堆的有序性。
删除最小元素, 删除根节点之后,第一步:取出列表最后的一个元素,移到根节点的位置,移动最后一个元素,保证了堆的结构,除了当前根节点可能不在正确的位置。
然后递归将根节点沿着树找到正确的位置,根节点交换 最小的子节点。重新获得堆的有序性。
class BinaryHeap: def __init__(self): self.heapList = [0] self.currentSize = 0 def peerUp(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 += 1 self.peerUp(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 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 self.heapList.pop() self.percDown(1) return retval def buildHeap(self, alist): i = len(alist) // 2 self.currentSize = len(alist) self.heapList = [0] + alist[:] while i > 0: self.percDown(i) i = i - 1
二叉搜索树
感兴趣不是元素在树中的确切位置,而是如何利用二叉树结构提供高效的搜索。
# coding=utf-8 class TreeNode: def __init__(self, key, val, left=None, right=None, parent=None): self.key = key self.payload = val self.leftChild = left self.rightChild = right self.parent = parent def hasLeftChild(self): return self.leftChild def hasRightChild(self): return self.rightChild def isLeftChild(self): return self.parent and self.parent.leftChild == self def isRightChild(self): return self.parent and self.parent.rightChild == self def isRoot(self): return not self.parent def isLeaf(self): return not (self.rightChild or self.leftChild) def hasAnyChildren(self): return self.rightChild or self.leftChild def hasBothChildren(self): return self.rightChild and self.leftChild def replaceNodeData(self, key, value, lc, rc): self.key = key self.payload = value self.leftChild = lc self.rightChild = rc if self.hasLeftChild(): self.leftChild.parent = self if self.hasRightChild(): self.rightChild.parent = self def findSuccessor(self): succ = None if self.hasRightChild(): # 有右子树,右子树里找个最小的,左子树, succ = self.rightChild.findMin() else: # 没有右子树, if self.parent: if self.isLeftChild(): # 其本身是父节点的左子节点。那么后继节点就是父节点 succ = self.parent else: # 是父节点的右子节点,其本身没有右子节点,那么后继节点就是除其本身外父节点的后继节点 self.parent.rightChild = None succ = self.parent.findSuccessor() self.parent.rightChild = self return succ def findMin(self): current = self while current.hasLeftChild(): current = current.leftChild return current def spliceOut(self): if self.isLeaf(): if self.isLeftChild(): self.parent.leftChild = None else: self.parent.rightChild = None elif self.hasAnyChildren(): if self.hasLeftChild(): if self.isLeftChild(): self.parent.leftChild = self.leftChild else: self.parent.rightChild = self.leftChild self.leftChild.parent = self.parent else: if self.isLeftChild(): self.parent.leftChild = self.rightChild else: self.parent.rightChild = self.rightChild self.rightChild.parent = self.parent def __iter__(self): if self: if self.hasLeftChild(): for elem in self.leftChild: yield elem yield self.key if self.hasRightChild(): for elem in self.rightChild: yield elem class BinarySearchTree: def __init__(self): self.root = None self.size = 0 def length(self): return self.size def __len__(self): return self.size def __iter__(self): return self.root.__iter__() def put(self, key, val): """ 插入新节点 """ if self.root: self._put(key, val, self.root) else: self.root = TreeNode(key, val) self.size = self.size + 1 def _put(self, key, val, currentNode): if key < currentNode.key: if currentNode.hasLeftChild(): self._put(key, val, currentNode.leftChild) else: currentNode.leftChild = TreeNode(key, val, parent=currentNode) else: if currentNode.hasRightChild(): self._put(key, val, currentNode.rightChild) else: currentNode.rightChild = TreeNode(key, val, parent=currentNode) def __setitem__(self, key, val): self.put(key, val) def get(self, key): if self.root: res = self._get(key, self.root) if res: return res.payload else: return None else: return None def _get(self, key, currentNode): if not currentNode: return None elif currentNode.key == key: return currentNode elif key < currentNode.key: return self._get(key, currentNode.leftChild) else: return self._get(key, currentNode.rightChild) def __getitem__(self, key): return self.get(key) def __contains__(self, key): if self._get(key, self.root): return True else: return False def delete(self, key): """ 如果只有1个节点,说明删除的是根节点, 还需要继续判断key是否相等 如果没找到,就抛出异常 找到待删除的key.有三种情况 1: 待删除的节点没有子节点 2: 待删除的节点有一个子节点 3: 待删除的节点有俩个子节点 """ if self.size > 1: nodeToRemove = self._get(key, self.root) if nodeToRemove: self.remove(nodeToRemove) self.size = self.size -1 else: raise KeyError(" Error, key not in the tree") elif self.size == 1 and self.root.key == key: self.root = None self.size = self.size -1 else: raise KeyError(" Error, key not in the tree") def remove(self, currentNode: TreeNode): if currentNode.isLeaf(): #1: 待删除的节点没有子节点 # 移除父节点的引用。可能是左,也可能是右,需要判断下。 if currentNode == currentNode.parent.leftChild: currentNode.parent.leftChild = None else: currentNode.parent.rightChild = None elif currentNode.hasBothChildren(): # 3: 有俩个子节点 # 找到候选节点, 候选节点要能为左右子树都保持二叉搜索树的关系。也就是树中具有次大键的节点。 也叫后继节点。 # 后继节点的子节点必定不会多余1个(其他俩个可能,已经实现)。 移除后继节点后,放到待删除的节点位置上。 succ = currentNode.findSuccessor() succ.spliceOut() currentNode.key = succ.key currentNode.payload = succ.payload else: # 2: 待删除的节点有一个子节点 # 可以用子节点取代待删除节点。 又细分6种情况 # 左右子节点是对称的 # 1) 如果当前节点是一个左子节点,只需将当前节点的左子节点对父节点的引用改为指向当前节点的父节点。父节点指向当前节点的左子节点 # 2)如果当前节点是一个右子节点,只需将当前节点的右子节点对父节点的引用改为指向当前节点的父节点。父节点指向当前节点的右子节点 # 3) 如果当前节点没有父节点,就是根节点,调用replaceNodeData方法,替换根节点的key、payload、leftChild、rightChild if currentNode.hasLeftChild(): # 有左子节点 if currentNode.isLeftChild(): # 当前节点是一个左子节点 currentNode.leftChild.parent = currentNode.parent currentNode.parent.leftChild = currentNode.leftChild elif currentNode.isRightChild(): # 当前节点是一个右子节点 currentNode.rightChild.parent = currentNode.rightChild currentNode.parent.rightChild = currentNode.leftChild else: # 根节点, 没有右子节点 currentNode.replaceNodeData(currentNode.leftChild.key, currentNode.leftChild.payload, currentNode.leftChild.leftChild, currentNode.leftChild.rightChild) else: # 有右子节点 if currentNode.isLeftChild(): currentNode.rightChild.parent = currentNode.parent currentNode.parent.leftChild = currentNode.rightChild elif currentNode.isRightChild(): currentNode.rightChild.parent = currentNode.parent currentNode.parent.rightChild = currentNode.rightChild else: currentNode.replaceNodeData(currentNode.rightChild.key, currentNode.rightChild.payload, currentNode.rightChild.leftChild, currentNode.rightChild.rightChild) def __delitem__(self, key): self.delete(key)
最坏的情况就是 只有左树或者右树。
平衡二叉树
当二叉搜索树不平衡时,性能降到O(n).
一种特殊的二叉搜索树, 自动维持平衡, AVL树。
与二叉树唯一的差别就是性能。
实现AVL树,要记录每个节点的平衡因子。定义为:左右子树的高度之差。
平衡因子大于零,左倾,
平衡因子小于零,右倾。
平衡因子等于零,完全平衡
平衡因子为-1, 0, 1 都定义为平衡树。
一旦某个节点的平衡因子超出这个范围。我们就需要通过一个过程让树恢复平衡。
在任何时间,AVL树的高度都等于节点树取对数再乘以一个常数1.44. 时间复杂度被限制为O(logN)
AVL树的实现
新键都是以叶子节点插入的。新叶子节点的平衡因子是零。
插入新节点后,必须更新父节点的平衡因子,
新的叶子节点对其父节点平衡因子的影响取决于它是左子节点还是右子节点
如果是右子节点,父节点的平衡因子减1,
如果是左子节点,父节点的平衡因子加1。
为了让AVL树恢复平衡,需要在树上进行1次或多次旋转
- 左旋:
步骤:
1、将右子节点提升为子树的根节点
2、将旧根节点作为新根节点的左子节点
3、如果新根节点已经有一个左子节点,将其作为新左子节点的右子节点。
因为节点B之前是节点A的右子节点,此时节点A必然没有右子节点。因此可以为其添加新的右子节点,无需多虑。 - 右旋:
步骤:
1、将左子节点提升为子树的根节点
2、将旧根节点作为新根节点的右子节点
3、如果新根节点已经有一个右子节点,将其作为新右子节点的左子节点。
因为节点C之前是节点E的左子节点,所以节点E必然没有左子节点。无需多虑。
class AVL(BinarySearchTree): def _put(self, key, val, currentNode: AVL): if key < currentNode.key: if currentNode.hasLeftChild(): self._put(key, val, currentNode.leftChild) else: currentNode.leftChild = TreeNode(key, val, parent=currentNode) self.updateBalance(currentNode.leftChild) else: if currentNode.hasRightChild(): self._put(key, val, currentNode.rightChild) else: currentNode.rightChild = TreeNode(key, val, parent=currentNode) self.updateBalance(currentNode.rightChild) def updateBalance(self, node: AVL): if node.balanceFactor > 1 or node.balanceFactor < -1: # 需要再平衡 self.reblance(node) return if node.parent != None: # 如果当前节点不需要再平衡,就调整父节点的平衡因子。 if node.isLeftChild(): node.parent.balanceFactor += 1 elif node.isRightChild(): node.parent.balanceFactor -= 1 if node.parent.balanceFactor != 0: self.updateBalance(node.parent) def rotateLeft(self, rotRoot: AVL): newRoot = rotRoot.rightChild rotRoot.rightChild = newRoot.leftChild if newRoot.leftChild != None: newRoot.leftChild.parent = rotRoot newRoot.parent = rotRoot.parent if rotRoot.isRoot(): self.root = newRoot else: if rotRoot.isLeftChild(): rotRoot.parent.leftChild = newRoot else: rotRoot.parent.rightChild = newRoot newRoot.leftChild = rotRoot rotRoot.parent = newRoot rotRoot.balanceFactor = rotRoot.balanceFactor + 1 - min(newRoot.balanceFactor, 0) newRoot.balanceFactor = newRoot.balanceFactor + 1 + max(rotRoot.balanceFactor, 0) def rotateRight(self, rotRoot: AVL): pass def rebalance(self, node): if node.balanceFactor < 0: if node.rightChild.balanceFactor > 0: self.rotateRight(node.rightChild) self.rotateLeft(node) else: self.rotateLeft(node) elif node.balanceFactor > 0: if node.leftChild.balanceFactor < 0: self.rotateLeft(node.leftChild) self.rotateLeft(node) else: self.rotateLeft(node)
恢复平衡最多需要旋转俩次。
图及其算法
树是特殊的图。
术语
- 顶点
节点,是图的基础部分,可以带有附加信息,有效载荷 - 边
俩个顶点通过一条边相邻。可以是单向的,双向的; - 权重
表示一个顶点到另一个顶点的成本。
图可以用G=(V,E)来表示。
V代表顶点:V={V0,V1,V2}
E代表边:E={(V0,V1,5)}
- 路径
由边连接的顶点组成的序列 - 环
起点和终点为同一个顶点的路径。
没有环的图称为无环图,
没有环的有向图称为有向无环图
图的抽象数据类型
Graph() 新建一个空图
addVertex(vert) 向图中添加一个顶点实例
addEdge(fromVert, toVert) 添加一条有向边。
addEdge(fromVert, toVert, weight) 添加一条带权重的有向边
getVertex(vertKey) 在图中找到名为vertKey的顶点
getVertices()以列表形式返回图中所有顶点
有俩种非常著名的图实现, 邻接矩阵和邻接表
邻接矩阵
实现图,最简单的方式就是使用二维矩阵,每一行和每一列都表示图中的一个顶点。
第v行和第w列交叉的格子中的值表示从顶点v到顶点w的边的权重。
邻接矩阵的优点是简单,但是如果矩阵是稀疏的,大部分单元格是空的,就很低效。
邻接表
为图对象的所有顶点保存一个主列表,
同时为每一个顶点对象都维护一个列表。记录了与它相连的顶点。
实现
- Vertex 顶点类
class Vertex: def __init__(self, key): self.id = key self.connectedTo = {} def addNeighbor(self, nbr, weight=0): self.connectedTo[nbr] = weight def __str__(self): return str(self.id) + ' connectedTo: ' + str([x.id for x in self.connectedTo]) def getConnections(self): return self.connectedTo.keys() def getId(self): return self.id def getWeight(self, nbr): return self.connectedTo[nbr]
- Graph 类,图
from vertex import Vertex class Graph: def __init__(self): self.vertList = {} self.numVertices = 0 def addVertex(self, key): self.numVertices = self.numVertices + 1 newVertex = Vertex(key) self.vertList[key] = newVertex return newVertex def getVertex(self, n): if n in self.vertList: return self.vertList[n] else: return None def __contains__(self, n): return n in self.vertList def addEdge(self, f, t, cost=0): if f not in self.vertList: nv = self.addVertex(f) if t not in self.vertList: nv = self.addVertex(t) self.vertList[f].addNeighbor(self.vertList[t], cost) def getVertices(self): return self.vertList.keys() def __iter__(self): return iter(self.vertList.values()) g = Graph() for i in range(6): g.addVertex(i) print("g:", g.vertList) g.addEdge(0, 1, 5) g.addEdge(0, 5, 2) for v in g: for w in v.getConnections(): print("(%s, %s)" % (v.getId(), w.getId()))
广度优先搜索
- 词梯问题,
将单词FOOL转换成SAGE.
每次只替换一个字母,并且每一步的结果都必须是一个单词,
FOOL POOL POLL POLE PALE SALE SAGE 。
在给定部署内完成转换,必须用到某个单词。
研究:从起始单词转换到结束单词所需的最小步数。
构造词梯图
第一个问题:如何用图来表示大的单词集合。
如果俩个单词的区别仅在于有一个不同的字母,就用一条边将它们相连。
如果能创建这样一个图,那么其中任意一条连接俩个单词的路径就是词梯问题的一个解。
-
笨方法
假设有一个单词列表,其中每个单词的长度都相同。为每个单词创建顶点。
为了连接这些顶点,可以将每个单词与列表中的其他所有单词进行比较。如果俩个单词只相差一个字母,就可以在图中创建一条边。将它们连接起来。
对于只有少量单词的情况,还行。如果列表中有很多单词,时间复杂度O(n^2) -
高效的方法
假设有数目巨大的桶,每一个桶都标有一个长度为4的单词,但是某一个字母被下划线代替。
一旦将所有单词都放入对应的桶中,同一个桶中的单词一定是相连的。
构建词梯
def buildGraph(wordFile): d = {} g = Graph() wfile = open(wordFile, 'r') # 创建词桶 for line in wfile: word = line[:-1] for i in range(len(word)): bucket = word[:i] + '_' + word[i+1:] 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
实现广度优先搜索
Breadth first search BFS
给定图G和起点s.
BFS通过边来访问G中与s之间存在路径的顶点。
特性:会访问完所有与s相距为k的顶点之后再去访问与s相距为k+1的顶点。
为了记录进度,BFS会将顶点标记为白色、灰色、黑色三种。
初始化,所有顶点都是白色。
当顶点第一次被访问时,标记为灰色,
当BFS完成对该顶点访问之后,标记为黑色,表示没有白色顶点与之相连
灰色顶点仍然可能与一些白色顶点相连。
顶点类新增三个实例参数
class Vertex: def __init__(self, key): self.id = key self.connectedTo = {} self.distance = 0 self.predecessor = None self.color = 'white' def setDistance(self, d): self.distance = d def getDistance(self): return self.distance def setPredecessor(self, vertex): self.predecessor = vertex def getPredecessor(self): return self.predecessor def setColor(self, color): self.color = color def getColor(self): return self.color
- 广度优先搜索
def bfs(g, start): start.setDistance(0) start.setPred(None) vertQueue = Queue() vertQueue.enqueue(start) while (vertQueue.size() > 0): currentVert = vertQueue.dequeue() for nbr in currentVert.getConnections(): if (nbr.getColor() == 'white'): nbr.setColor('gray') nbr.setDistance(currentVert.getDistance() + 1) nbr.setPredecessor(currentVert) vertQueue.enqueue(nbr) currentVert.setColor('black') def traverse(y): x = y while(x.getPred()): print(x.getId()) x = x.getPredecessor() print(x.getId())
深度优先搜索
骑士周游问题
国际象棋棋盘和一颗骑士妻子。找到一系列走法,使得骑士对棋盘上的每一格刚好都只访问依次。
这样的一个移动序列被称为: 周游路径。
构建完整的图
def knightGraph(bdSize): ktGraph = Graph() for row in range(bdSize): for col in range(bdSize): nodeId = posToNodeId(row, col, bdSize) newPositions = genLegalMoves(row, rol, bdSize) for e in newPositions: nid = posToNodeId(e[0], e[1]) ktGraph.addEdge(nodeId, nid) return ktGraph def posToNodeId(row, column, board_size): """将行列位置转换成数 """ return (row * board_size) + column def genLegalMoves(x, y, bdSize): """ 接受骑士在棋盘上的位置,生成8中可能走法。 创建一个列表,用于记录从这一格开始的所有合理走法。 之后,所有的合理走法都被转换成图中的边 """ newMoves = [] 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, bdSize) and legalCoord(newY, bdSize) newMoves.append((newX, newY)) return newMoves def legalCoord(x, bdSize): """确认走法是否合理""" if x >= 0 and x < bdSize: return True else: return False
def knightTour(n, path, u, limit): u.setColor('gray') path.append(u) if n < limit: nbrList = list(u.getConnections()) 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 += 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]
nbrList = list(u.getConnections()) 改为 nbrList = orderByAvail(u)
通用深度优先搜索
class DFSGrapth(Graph): def __init__(self): super().__init__() self.time = 0 def dfs(self): for aVertex in self: aVertex.setColor('white') aVertex.setPred(-1) for aVertex in self: if aVertex.getColor() == "white": self.dfsvisit(aVertex) def dfsvisit(self, startVertex): startVertex.setColor('gray') self.time += 1 startVertex.setDiscovery(self.time) for nextVertex in startVertex.getConnections(): if nextVertex.getColor() == 'white': nextVertex.setPred(startVertex) self.dfsvisit(nextVertex) startVertex.setColor('black') self.time += 1 startVertex.setFinish(self.time)
拓扑排序
拓扑排序根据有向无环图生成一个包含所有顶点的线性序列。
有向无环图表明事件优先级。
拓扑排序是对深度优先搜索的一种简单而强大的改进。
1)对图g调用dfs(g),计算每一个顶点的结束时间。
2)基于结束时间,将顶点按照递减顺序存储在列表中
3)将有序列表作为拓扑排序的结果返回
强连通分量
对于规模庞大的图。
网页当做顶点,超链接当做连接顶点的边。
网络具有一种基础的结构,使得在某种程度上相似的网页相互聚集。
通过一种叫做强连通分量的图算法,可以找出图中高度连通的顶点
最短路径问题
Dijkstra算法
用于确定最短路径,是一种迭代算法,可以提供从一个顶点到其他所有顶点的最短路径。
def dijkstra(aGraph, start): pq = PriorityQueue() # 优先级队列 start.setDistance(0) pq.buildHeap( [(v.getDistance(), v) for v in aGraph]) # 构造优先级队列,距离顶点的距离作为优先级。 while not pq.isEmpty(): currentVert = pq.delMin() for nextVert in currentVert.getConnections(): newDist = currentVert.getDistance() + currentVert.getWeight(nextVert) if newDist < nextVert.getDistance(): nextVert.setDistance(newDist) nextVert.setPred(currentVert) pq.decreaseKey(nextVert, newDist) # 当一个顶点的距离减少并且该顶点已经在优先级队列中,就调用这个方法。从而将该顶点移向优先级队列的头部。
Dijkstra算法只适用于边的权重均为正的情况,如果有权重为负,那么永远不会退出。
Prim算法
高效的把信息传递给所有人,广播消息。
解决广播问题的关键在于构建一颗权重最小的生成树。
广播服务器只需向网络中发送一条消息副本,每一个路由器向属于生成树的相邻路由器转发消息,其中不包括刚刚向它发送消息的路由器。
每一个路由器都只看到任意消息的一份副本,所有收听者都接收到了消息。
这个思路对应的算法叫做prim算法。每一步都选择代价最小的下一步,属于贪婪算法。
from queue import PriorityQueue import sys def prim(G, start): pq = PriorityQueue() for v in G: v.setDistance(sys.maxsize) v.setPrec(None) start.setDistance(0) pq.buildHeap( [ (v.getDistance(), v) for v in G]) while not pq.isEmpty(): currentVert = pq.delMin() for nextVert in currentVert.getConnections(): newCost = currentVert.getConnections() + currentVert.getDistance() if v in pq and newCost < nextVert.getDistance(): nextVert.setPred(currentVert) nextVert.setDistcance(newCost) pq.decreaseKey(nextVert, newCost)
附加内容
- 取余加密函数
def encrypt(m): '''凯撒加密''' s = 'abcdefghijklmnopqrstuvwxyz' n = '' for i in m: j = (s.find(i) + 13) % 26 n = n + s[j] return n
加密:uryybibeyq hellovorld
因为字母表有26个字母,所以这个函数是对称的,
对称性是指可以用同一个函数进行加密和解密。
如果用其他数字,就不行了。
- 解密
def decrypt(m , k): s = 'abcdefghijklmnopqrstuvwxyz' n = '' for i in m: j = (s.find(i) + 26 - k) % 26 n = n + s[j] return n
- 同余定理
俩个数a和b除以n所得的余数相等,我们就说a和b 对模 n同余。记为a == b(mod n)
1) a + c = b + c(mod n)
- ac = bc(mod n)
- a^p = b^p (mod n)
- 幂剩余
假设我们想知道3^1254906的最后一位数,
如何高效的计算。
如何能在不必算出所有位数的前提下,计算x^n(mod p)
1)将result初始化为1
2)重复n次
用result乘以x
对result进行取余运算
def modexp(x, n, p): ''' x^n (mod p) ''' if n == 0: return 1 t = (x * x) % p tmp = modexp(t, n // 2, p) if n % 2 != 0: tmp = (tmp * x ) % p return tmp
RSA算法
是最易理解的公钥加密算法。
主要贡献是在于密钥成对的思想: 加密密钥用于明文到密文的转换。解密密钥用于密文到明文的转换。
密钥都是单向的。
RSA算法的安全性来自于大数分解的难度。公钥和私钥由一对大素数(100-200位)得到。
要生成俩个密钥,选俩个大素数p和q.并计算它们的乘积
n = p * q
下一步是随机选择加密密钥e, 使得 e与 (p-1) * (q-1) 互素
gcd(e,(p-1) * (q-1))) = 1
加密密钥d就是e关于模(p-1)*(q-1)的逆元。
e和n一起组成公钥。 d则是私钥。
加密: c = m^e(mod n)
解密: m = c^d(mod n)
如何将hello world这样的文本信息转换成数字?最简单的方法就是把每个字符的ASCII值拼接起来。
不过由于十进制位数不固定。采用十六进制。
拼接所有的十六进制数,再讲得到的大数转换为十进制整数。
m = 126207244316550804821666916
实际使用RSA算法加密的程序会将明文切分成小块。
对每一块加密。有俩个原因,第一个原因是性能,即使一条较短的电子邮件消息。比如大小为1k的文本。生成的数也会有2000-3000位。再取d(10位)次方。是一个非常大的数。
第二个原因是限制条件 m <= n .必须确保消息的模n表示是唯一的。
举例来说: p=5563 , q = 8191 . n = 5563 * 8191 = 45566533.
为了保持分块的整数小于m. 我们将消息切分成小于表示n所需的字节数。
'hel' = 6841708
现在选择e的值。 可以随机取值。并用 (p-1)* (q-1) = 45552780 检验。互素。 e = 1471
d=ext_gcd(45552780, 1471) = 33847171
加密'hel' : c = 6841708^1471 ( mod 45566533) = 16310024
解密: m = 16310024^33847171 ( mod 45566533) = 6841708
跳表
可以使用散列实现映射,因为表的大小、冲突、冲突解决策略等因素而性能降低
使用二叉搜索树实现映射,键可能左倾或右倾,搜索性能随之下降。
解决办法:跳表
跳表是二维的,向右和向下。表头在左上角,是跳表的唯一入口。
键 值 down next 四部分组成。
最左边是头节点,右边是数据节点
由数据节点构成的纵列称作塔。塔的高度不一。
每个链表都有自己的名字,用层数指代。层数从0开始,底层就是第0层。包括整个节点集合。每个键值对必须出现在第0层的链表中。
层数越高,节点数就越少。
跳表的这个重要特征有助于提高搜索效率。
头节点 由next和down俩个引用构成。
class HeaderNode: def __init__(self): self.next = None self.down = None def getNext(self): return self.next def getDown(self): return self.down def setNext(self, newnext): self.next = newnext def setDown(self, newDown): self.down = newDown class DataNode: def __init__(self, key, value): self.key = key self.data = value self.next = None self.down = None def getKey(self): return self.key def getData(self): return self.data def getNext(self): return self.next def getDown(self): return self.down def setData(self, newdata): self.data = newdata def setNext(self, newnext): self.next = newnext def setDown(self, newdown): self.down = newdown class SkipList: def __init__(self): self.head = None
- 搜素跳表
输入一个键,从表头,最上面一层开始,如果当前键小于目标键,向前移动,如果没有键,就向下移动。
因为每一层都是有序的列表。所以不匹配的键提供了很有用的信息。
def search(self, key): current = self.head found = False stop = False while not found and not stop: if current == None: # 没有数据了 stop = True else: if current.getNext() == None: # 向前没有node,就向下 current = current.getDown() else: if current.getNext().getKey() == key: # 当前key 和目标key一致 found = True else: if key < current.getNext().getKey(): # 下一个比key大。向下走。 current = current.getDown() else: # 下一个比key小,向右走 current = current.getNext() if found: return current.getNext().getData() else: return None
- 往跳表中加入键-值对
如何构建跳表
1)搜素跳表,寻找插入位置
2)新建一个数据节点,并加入到0层的立案表中。还需要为新的数据节点构建塔。这就是跳表的有趣之处,塔应该多高。新建数据点的塔高并不确定,而是完全随机。
def flip(self): return randrange(2) def insert(self, key, data): if self.head == None: # 当前表示空的。 self.head = HeaderNode() temp = DataNode(key, data) top = temp while self.flip() == 1: # 抛硬币,如果==1 ,就建塔。直到不是1. newhead = HeaderNode() temp = DataNode(key, data) temp.setDown(top) newhead.setNext(temp) newhead.setDown(self.head) self.head = newhead top = temp else: towerStack = Stack() current = self.head stop = False while not stop: if current == None: stop = True else: if current.getNext() == None: towerStack.push(current) current = current.getDown() else: if current.getNext().getKey() > key: towerStack.push(current) current = current.getDown() else: current = current.getNext() lowestLevel = towerStack.pop() temp = DataNode(key, data) temp.setNext(lowestLevel.getNext()) lowestLevel.setNext(temp) top = temp while self.flip() == 1 : if towerStack.isEmpty(): newhead = HeaderNode() temp = DataNode(key, data) temp.setDown(top) newhead.setNext(temp) newhead.setDown(self.head) self.head = newhead top = temp else: nextLevel = towerStack.pop() temp = DataNode(key, data) temp.setDown(top) temp.setNext(nextLevel.getNext()) nextLevel.setNext(temp) top = temp
复习树:量化图片。
数字图像,由数以千计的像素组成。像素排列成矩阵,形成图像。每个像素代表一个特定的元素。
物理世界中,不同颜色之间的过渡是连续的。人眼可以区分200种不同的层次。总共约800万种颜色。
python使用元组列表来表示图片。 3个 0-255数字构成。
量化图片
节省图片的存储空间。最简单的就是减少所用颜色的种类。颜色越少,红绿蓝的位数就越少。
如何将颜色从1670万种降到256中呢? 量化
将256256256 的 立方体转换成 888的立方体。
- 简单的图片量化算法
import sys import os import Image def simpleQuant(): im = Image.open('bubbles.jpg') w,h = im.size for row in range(h): for col in range(w): r,g,b = im.getpixel((col, row)) r = r // 36 * 36 g = g // 42 * 42 b = b // 42 * 42 im.putpixel((col, row), (r,g,b)) im.show()
- 八叉树改进的量化算法
simpleQuant的问题在于,大部分图片中的颜色不是均匀分布的。很多颜色可能没有出现在图片中。所以立方体中对应的部分并没有用到。
在量化后的图片中分配没用到的颜色是浪费行为。
为了更好地量化图片。选出表示图片时用到的颜色集合。
有多种算法可用于切分立方体,以更好地使用颜色。
基于八叉树的算法
每个节点右8个子节点。 - OctTree()新建一颗空的八叉树
- insert(r,g,b) 往八叉树中插入一个节点。 以红、绿、蓝的值为键。
- find(r, g, b) 以红绿蓝的值为搜索键,查找一个节点,或与其最相似的节点。
- reduce(n) 缩小八叉树,使其有n个或更少的叶子节点。
OctTree通过如下方式切分颜色立方体
- OctTree的根代表整个立方体
- OctTree的第二层代表每个维度(x,y,z)上的一个切片,将立方体等分成8块
- 下一层将8个字块中的每一块再等分成8块,即64块。。。父节点代表的方块包含其子节点代表的所有子块。沿着路径往下,字块始终位于父节点所规定的界限内。但会越来越具体。
- 八叉树的第8层代表所有颜色。约1670万种。
八叉树是分级的,可以利用其层级,用大立方体表示其未使用的颜色,用小立方体表示常用颜色。
1)针对图片中的每个像素,执行以下操作
a) 在OctTree中查找该像素的颜色,这个颜色应该是位于第8层中的一个叶子节点
b)如果没找到,就在第8层创建一个叶子节点。
c)如果找到了,将叶子节点中的计数器加1,以记录这个颜色用于多少个像素。
2)重复以下步骤,直到叶子节点的数目小于或等于颜色的目标数目。
a)找到用得最小的叶子节点
b) 合并该叶子节点及其所有兄弟节点。从而形成一个新的叶子节点。
3)剩余的叶子节点形成图片的颜色集
4)若要将初始的颜色映射为量化后的值,只需沿着树向下搜索到叶子节点,然后返回叶子节点存储的颜色值。
复习图:模式匹配
在长字符串中寻找模式, 这种模式被称作子串。
暴力法太慢了。
一种改善措施是,如果不匹配,就以多于一个字母的幅度滑动模式。
没有利用
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· Docker 太简单,K8s 太复杂?w7panel 让容器管理更轻松!