3-1复习最短路径算法,3-2学习二叉数结构,3-4并查集合
第7章,神奇的树。
第一节,树的特点。
1.一个节点到另一个节点只有唯一路径
2.n个节点n-1个边。
3.在一颗树中加一条边会构成回路
第二节,二叉树。
要么为空,要么由根节点,左子树和右子树组成,而左子树和右子树分别是一棵二叉树。
- 完全二叉树: 最右边位置有一个或几个叶节点缺少外,其他是丰满的。即第h层节点有缺少,其他层都不少。
- 满二叉树: 所有父节点都有2个子节点。即有h层,并且有2h-1个节点。
完全二叉树 | 满二叉树 | |
总节点数k | 2h-1 <= k < 2h - 1 | k = 2h-1 |
树高h | h = logk + 1 | h= log(K+1) |
如果父节点k,则左子点2k,右子点2k+1.
如果子节点x,则父节点是x/2。(因为x是整数,因此,实际上计算得到的x/2没有了小数部分)
例: 子节点是5, 父节点是2 (5/2等于2).
只需要一个一维数组即可储存完全二叉树。
第三节,优先队列--堆(特殊的完全二叉树)
最小堆:All node-father smaller than it's node-sons 所有父节点小于子节点
最大堆:相反。
问题:如果删除一组数中最小数,再增加一个新数,怎样快速的求得这些数中最小的一个数?
因为用遍历的方法需要N次比较,太费时。
所以用堆来解:h次比较。h是堆的层数。时间复杂度是h.
方法:
- 首先把这组数字,按照最小堆的结构进行排列。
- 删除第一数字,即最小数。然后在这个位置增加一个新数字。此时可能已经不符合最小堆特性。
- 把新添加的数字向下👇调整。直到重新符合最小堆的特性。
- 从第一层根节点开始判断,第一个父节点和它的2个子节点比较。
- 先和左儿子比较,再和右儿子比较。用变量temp记录最小的节点编号。
- 如果最小节点不是父节点自身,那么父节点和其中值最小的儿子交换位置。
- 这个调整下来的父节点,继续和下一层的儿子们比较,
- 重复234过程,直到到达最底部。
理解:脑子里先形成图像,并画出来,然后按照图像的变化的特性,编写代码。
def shiftdown(i, array) flag = 0 #用于判断是否退出while循环 temp = 0 #临时变量 n = array.size - 1#总节点数,或最后一个节点编号。 # 默认传入的参数i是1。传入的数组是一个最小堆。 # 循环是一层一层的进行直到到堆的最下层(用i*2 > n来判断 , 它的意思是子节点编号不可能大于n, 因为n是最后一个节点编号)就结束循环。 while i*2 <= n && flag == 0 #首先父节点和左子节点比较,用temp记录较小的节点编号 if array[i] > array[i*2] temp = i*2 else temp = i end #如果存在右儿子,继续比较,目的是找到父子中最小的那个记录下来。 if i*2+1 <= n if array[temp] > array[i*2+1] temp = i*2+1 end end #通过上面的2个判断,如果发现最小节点不是父节点,把最小节点和父节点交换位置! if temp != i t = array[i] array[i] = array[temp] array[temp] = t # 别忘记把节点编号赋值给变量i。这代表下一次的循环开始: i = temp else # 如果最小节点就是父亲,已经符合最小堆特性,无需再下一个循环 flag = 1 end end return array end a = [nil,1, 2, 5, 12, 7, 17, 25, 19, 36, 99, 22, 28, 46, 92] p "输入数值:\n" a[1] = gets.to_i p shiftdown(1, a)
问题2: 如果只想要新增加一个值,如何在原来的堆上直接插入一个新增的值,并保持堆的结构?
答案:最小堆的结构是:父节点一定小于儿子节点。所以把新增值插入到末尾(即最后的叶节点),然后和它的父节点比大小,如果小于父节点就上移,然后继续和上层的父节点比较,直到顶层。
def shiftup(i, array) #要判断的节点x x = i #是否关闭循环 close = false while x != 1 && close == false if array[x] < array[x/2] #交换位置 temp = array[x] array[x] = array[x/2] array[x/2] = temp #下一轮循环的节点号: x = x/2 else #结束shifup方法 close = true end end return array end a = [nil,1, 2, 5, 12, 7, 17, 25, 19, 36, 99, 22, 28, 46, 92] puts "输入数值:\n" a << gets.to_i puts "你输入了一个值:#{a.last}" n = a.size - 1 puts shiftup(n, a)
问题3: 如何建立最小堆?
方法1:
从空堆开始,依次插入每个元素,并运行shiftup方法,以便符合堆的结构。直到所有数都被插入到堆中。
时间复杂度:NlogN
如果有N个数,插入第i个数字所用时间是logi, 所以插入所有元素的时间复杂度,最大值是N*logN。
代码:
def shiftup(i, array) #要判断的节点x x = i #是否关闭循环 close = false while x != 1 && close == false if array[x] < array[x/2] #交换位置 temp = array[x] array[x] = array[x/2] array[x/2] = temp #下一轮循环的节点号: x = x/2 else #结束shifup方法 close = true end end return array end #要建立的堆的无需数组: a = [nil, 4, 2, 10 , 1, 34, 20] # n: 总共6个元素: n = a.size - 1 #建立一个空数组,用来储存堆 h = [nil] #从空堆开始插入,插入n次 a.each_index do |i| if i == 0 next end h << a[i] number = h.size - 1 shiftup(number, h) end puts h
方法2: 建立最小堆。
时间复杂度是: N。
完全二叉数有一个性质:最后一个非叶节点是第n/2个节点。
把数据按照完全二叉树结构编码,然后从最后一个非叶节点开始到根节点,逐个扫描所有的节点,根据需要对当前节点向下调整shiftdown().最后产生符合最小堆的数据结构。
#要建立的堆的无需数组: a = [nil, 4, 2, 10 , 1, 34, 20] # n: 总共6个元素: n = a.size - 1 # n/2是最后的非叶节点, 扫描所有非叶节点,并以每个非叶节点为根节点向下调整成堆。 # 要传入shiftdown的参数 i = n/2 1.upto(n/2) do shiftdown(i, a) i = i - 1 end puts a
堆的另一个作用:堆排序
时间复杂度和快速排序法一样。
- 如果想要把一个数组从小到大排序,首先建立最小堆,如上代码。
- 把根节点数据输出或放入新的数组中。
- 再对剩下数据建立最小堆。⚠️这里不是从新建立最小堆,而是取巧,把剩下的最后一个数据放到堆顶,然后使用shifitdown方法形成最小堆。
- 重复2和3。
- 输出的新数组就是从小到大的排序。
# 删除最小节点元素,并重新向下调整成最小堆
⚠️:还有一种简单的方法:
sorted_a = [nil] while a.size - 1 > 0 a = create_heap(a) sorted_a << a[1] a.delete(a[1]) end puts sorted_a
这种方法,因为重新把一个无需数组排序,所花费时间比上面的方法更长。所以放弃。
堆排序的另外一种方法
解题思路:
如何要生成最大堆:
- 首先,先把数组做出完全二叉树结构。最大堆是指父亲必须大于儿子。
- 然后,类似于建立的方法最小堆,对每个非叶节点从最后开始都做向下调整。
- ⚠️向下调整方法shiftdown_max方法:父节点和子节点比较,如果父亲不是最大的,父节点和最大的子节点交换。
def shiftdown_max(i, array) flag = 0 temp = 0 #节点数 n = array.size - 1 while 2*i <= n && flag == 0 if array[i] > array[2*i] temp = i else temp = 2*i end if 2*i+1 <= n #右儿子存在 if array[temp] < array[2*i + 1] temp = 2*i + 1 end end if temp != i t = array[i] array[i] = array[temp] array[temp] = t # 用于下一轮循环 i = temp else #无需交换,也无需向下了,结束循环 flag = 1 end end return array end a = [nil,23, 2,55, 111, 7, 17, 25, 19, 26, 9] #最后一个非叶节点 i = (a.size-1)/2 while i > 0 shiftdown_max(i, a) i -= 1 end puts a
⚠️,遇到问题可以使用byebug xxx.rb除错。
如何从小到大排序最大堆:
- 把堆顶和最后一个叶节点数据交换,把最后的节点存入一个新建的数组。
- 然后以前n-1个数为堆向下调整成最大堆。
- 重复1和2的操作。
- 最后就会得到一个排序的数组。
#原数组的元素数: n = a.size - 1 new_array = [] while n >= 1 temp = a[1] a[1] = a[n] a[n] = temp new_array.unshift(a.last) a.pop # 对前n-1个数向下调整,成最大堆。 # i是最后的非叶节点: i = (n-1)/2 while i > 0 shiftdown_max(i, a) i -= 1 end n = n - 1 end
p new_array
⚠️这里使用了Array#unshift和pop方法。因此可以按照从大到小,或从小到大排序。
⚠️有了思路要实现成代码,还要熟用相关语言。Ruby和c的实现完全不同,因为Ruby有很多语法糖可用。
总结:
支持插入元素和寻找最大(小)值的元素数据结构称为优先队列。堆就是一种优先队列。 即比普通队列能够更快的实现上面的2种功能。
另外,堆还可以查找一个数列中第k大(小)的数。
问题: 如何查找一个数列中第k大的数?
时间复杂度:NlogK
- 随意找k个数字建立一个大小为k的最小堆,此时堆顶是k个数中最小的。
- 从剩下的其他数找一个数字和堆顶比较,如果是比堆顶大的数就替换,然后向下调整成最小堆,否则就放弃。
- 重复2的步骤。
- 所有的数字都和堆顶比较完后,堆顶就是第k大的数。
- ⚠️它的原理是,堆顶是堆内最小的数字,其他不在堆内的数字都比堆顶的还小,所以堆顶是第k大的数字。
#找一个数组中第k大的数字。 a = [nil,23, 2,55, 111, 7, 17, 25, 19, 26, 9]
def shiftdown(i, array) #原理: 父节点一定比儿子小。 #方法: 找到最小的节点编号,如果这个编号的节点不是父本身,则这个编号的节点和父节点交换位置。然后继续向下层比较,直到底部。 flag = 0 #用于循环条件判断,为true则结束循环 temp = 0 #记录节点编号 n = array.size - 1 while 2*i <= n && flag == 0 if array[i] < array[2*i] temp = i else temp = 2*i end if 2*i + 1 <= n if array[temp] > array[2*i + 1] temp = 2*i + 1 end end if temp != i t = array[temp] array[temp] = array[i] array[i] = t #进行下一层的比较 i = temp else #结束循环。 flag = 1 end end return array end
#1. 首先, 用k个数建立一个最小堆。 puts "输入一个数:" k = gets.to_i # b是要做堆的数组 b = a[0..k] # 它的最后一个非叶节点: k/2 i = k/2 #建立最小堆: while i > 0 b = shiftdown(i, b) i -= 1 end puts "k个数建立的最小堆是:#{b}" #2.剩下的数字 n = a.size - 1 c = a[k+1..n] puts "要比较的数组:#{c}" #把比较完的非堆数字保存在这里: left_element = [] # 和堆顶比较 c.each do |c| if c > b[1] left_element << b[1] #替换 b[1] = c i = (b.size - 1)/2 #向下调整为最小堆 while i > 0 b = shiftdown(i, b) i -= 1 end else left_element << c end end puts "不在堆中的数字:#{left_element}" puts "堆:#{b}" puts "第#{k}大数字是:#{b[1]}"
问题: 如何查找一个数列中第k小的数?
- 随意找k个数字建立一个大小为k的最大堆。最后让堆顶就是第k小的数字。
- 从剩下的其他数字种找一个数字和堆顶比较,如果是比堆顶的小的数字就替换,然后向下调整成最大堆。否则放弃。
- 重复2步骤。
- 所有的数字都和堆顶比较完后,堆顶就是第k小的数。
原理:不在堆的数字都比堆顶大,同时堆是堆大堆,所以堆顶就是第k小的数字。
代码略:参考上面的代码,只需要调整shfitdown方法的父子比较取值。然后调整c.each中的一个比较符号。
结语:
参考:https://www.cnblogs.com/yangecnu/p/Introduce-Priority-Queue-And-Heap-Sort.html
本文介绍了二叉堆,以及基于二叉堆的堆排序,他是一种就地(原地)的非稳定排序,其最好和平均时间复杂度和快速排序相当,但是最坏情况下的时间复杂度要优于快速排序。
但是由于他对元素的操作通常在N和N/2之间进行,所以对于大的序列来说,两个操作数之间间隔比较远,对CPU缓存利用不太好,故速度没有快速排序快。
术语:
原地(就地)排序:在排序过程中无需申请多余的存储空间。只利用原来存储待排数据的存储空间进行比较和交换的数据排序。
非原地排序:需要利用额外的数组来辅助排序。
稳定排序:如果 a 原本在 b 的前面,且 a == b,排序之后 a 仍然在 b 的前面,则为稳定排序。
非稳定排序:如果 a 原本在 b 的前面,且 a == b,排序之后 a 可能不在 b 的前面,则为非稳定排序。
第四节 并查集 (merge-find-sets)
--也称union-find data structure
结构:
一种树型的数据结构。
用途/目的:
用于处理不相交集合(disjoint-sets)的查询和合并。
⚠️
一个集合必然要有一个共同的根节点/代表(boss/ancestor)
这种算法叫做union-find algorithm
- Find:确定元素属于哪一个子集。它可以被用来确定两个元素是否属于同一子集。
- Union:将两个子集合并成同一个集合。
为了更加精确的定义这些方法,需要定义如何表示集合。一种常用的策略是为每个集合选定一个固定的元素,称为代表,以表示整个集合。接着,Find(x) 返回 x 所属集合的代表,而 Union 使用两个集合的代表作为参数。
并查集森林,一种将每一个集合以树表示的数据结构,其中每一个节点保存着到它的父节点的引用,其中有2种优化,本章学习了“路径压缩”一种。
路径压缩:
是一种在执行“查找”时扁平化树结构的方法。关键在于在路径上的每个节点都可以直接连接到根上;Find
递归地经过树,改变每一个节点的引用,到根节点。得到的树将更加扁平,为以后直接或者间接引用节点的操作加速。这儿是Find
:
functionFind(x) if x.parent != x x.parent := Find(x.parent) return x.parent
def get_boss(i) # 如果元素的祖宗是自身,返回自身 # 如果不是是自身,沿着树向上找这个元素的祖先。 if $gang[i] == i return i else #通过递归,取找i元素的祖宗。并查集的数据的结构将是一种树结构,一个集合必然要有一个共同的祖先。 return $gang[i] = get_boss($gang[i]) end end def merge(a, b) #find: 找到传入的元素的祖宗(boss) #merge:如果二者没有共同的祖宗boss, 则合并(靠左原则),让它们有共同的一个祖宗。 t1 = get_boss(a) t2 = get_boss(b) if t1 != t2 return $gang[t2] = t1 end end # 表示一共有10个元素。分别用数字表示1~10 n = 10 # m表示有9条相关线索 m = 9 # 两个数组代表元素的关系,即a[1]和b[1]是相关的,同理a[i]和b[i]是相关的。总共9条关联。 a = [nil,1,3,5,4,2,8,9,1,2] b = [nil,2,4,2,6,6,7,7,6,4] # 首先假设每个元素都是不相关的, 索引代表自身,值代表它的上一父节点。 $gang = [nil,1,2,3,4,5,6,7,8,9,10] # 找到并合并 1.upto(9) do |i| merge(a[i], b[i]) puts "" p "#{i}: #{$gang}" end #统计有几个集合:统计根节点总数。 sum = 0 1.upto(n) do |i| if $gang[i] == i sum += 1 end end p "共计:#{sum}个集合"