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.

  

方法: 

  1. 首先把这组数字,按照最小堆的结构进行排列。
  2. 删除第一数字,即最小数。然后在这个位置增加一个新数字。此时可能已经不符合最小堆特性。
  3. 把新添加的数字向下👇调整。直到重新符合最小堆的特性。
    1. 从第一层根节点开始判断,第一个父节点和它的2个子节点比较。
    2. 先和左儿子比较,再和右儿子比较。用变量temp记录最小的节点编号。
    3. 如果最小节点不是父节点自身,那么父节点和其中值最小的儿子交换位置。
    4. 这个调整下来的父节点,继续和下一层的儿子们比较,
    5. 重复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().最后产生符合最小堆的数据结构。 

 

# 传入一个数组,内含无序数字,
⚠️ :传入的数组索引0位置,值必须是nil.否则会出现:❌
 
#要建立的堆的无需数组:
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

 

 

 

堆的另一个作用:堆排序

时间复杂度和快速排序法一样。

  1.  如果想要把一个数组从小到大排序,首先建立最小堆,如上代码。
  2.  把根节点数据输出或放入新的数组中。
  3.  再对剩下数据建立最小堆。⚠️这里不是从新建立最小堆,而是取巧,把剩下的最后一个数据放到堆顶,然后使用shifitdown方法形成最小堆。
  4. 重复2和3。
  5.  输出的新数组就是从小到大的排序。
# i节点向下调整,成最小堆
def shiftdown(i,array)
  flag = 0
  temp = 0
  n = array.size - 1
  while i*2 <= n && flag == 0
    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
    if temp != i
      t = array[i]
      array[i] = array[temp]
      array[temp] = t
 
      i = temp
    else
      flag = 1
    end
  end
  return array
end

 

# 创建最小堆
def create(array)
  n = array.size - 1
  i = n/2
  while i >= 1
    array = shiftdown(i, array)
    i -= 1
  end
  return array
end

 

# 删除最小节点元素,并重新向下调整成最小堆 

def deletemin(array)
  n = array.size - 1
  array[1] = array[n]
  array.delete_at(n)
  return shiftdown(1, array)
end
 
# 读入要排序的数组
a = [nil,236, 2,55, 111, 7, 17, 25, 19, 26, 9]
dui = create(a)
# 把顶部元素存入x数组,然后用最后的叶元素替换顶部元素,再向下调整成最小堆。
# 反复n次。相当于每次输出最小元素。x是从小到大排序。
n = dui.size - 1
x = [nil]
n.times do
  x << dui[1]
  dui = deletemin(dui)
end
p x

⚠️:还有一种简单的方法:

sorted_a = [nil]

while a.size - 1 > 0
  a = create_heap(a)
  sorted_a << a[1]
  a.delete(a[1])
end

puts sorted_a

 

这种方法,因为重新把一个无需数组排序,所花费时间比上面的方法更长。所以放弃。

 


堆排序的另外一种方法

解题思路:

如何要生成最大堆: 

  1. 首先,先把数组做出完全二叉树结构。最大堆是指父亲必须大于儿子。
  2. 然后,类似于建立的方法最小堆,对每个非叶节点从最后开始都做向下调整。
  3. ⚠️向下调整方法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除错。

 

如何从小到大排序最大堆:

  1. 把堆顶和最后一个叶节点数据交换,把最后的节点存入一个新建的数组。
  2. 然后以前n-1个数为堆向下调整成最大堆。
  3. 重复1和2的操作。
  4. 最后就会得到一个排序的数组。
#原数组的元素数:
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有很多语法糖可用。

 

教程使用了全局变量$n,  
# 生成最大堆
def create(array)
  i = $n/2
  while i >= 1
    array = shiftdown_max(i, array)
    i -= 1
  end
  return array
end
 
# 最大堆排序从小到大
def sort(array)
  while $n > 1
    t = array[1]
    array[1] = array[$n]
    array[$n] = t
    $n -= 1
    array = shiftdown_max(1, array)
  end
  return array
end
 
a = [nil,23, 2,55, 111, 7, 17, 25, 19, 26, 9]
$n = a.size - 1
# 最大堆
a = create(a)
p "最大堆#{a}"
p "从小到大排序#{sort(a)}"
# 总结:堆数组排序,如果是要从小到大,可以先生成最大堆,然后再排序。

 

# 如果从大到小,可以生成先最小堆。然后排序方法一样。
 

 

总结:

支持插入元素和寻找最大(小)值的元素数据结构称为优先队列。堆就是一种优先队列。 即比普通队列能够更快的实现上面的2种功能。

另外,堆还可以查找一个数列中第k大(小)的数。

 

问题: 如何查找一个数列中第k大的数?

时间复杂度:NlogK

  1. 随意找k个数字建立一个大小为k的最小堆,此时堆顶是k个数中最小的。
  2. 从剩下的其他数找一个数字和堆顶比较,如果是比堆顶大的数就替换,然后向下调整成最小堆,否则就放弃。
  3. 重复2的步骤。
  4. 所有的数字都和堆顶比较完后,堆顶就是第k大的数。 
  5. ⚠️它的原理是,堆顶是堆内最小的数字,其他不在堆内的数字都比堆顶的还小,所以堆顶是第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小的数?

  1. 随意找k个数字建立一个大小为k的最大堆。最后让堆顶就是第k小的数字。
  2. 从剩下的其他数字种找一个数字和堆顶比较,如果是比堆顶的小的数字就替换,然后向下调整成最大堆。否则放弃。
  3. 重复2步骤。
  4. 所有的数字都和堆顶比较完后,堆顶就是第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}个集合"

 

 

 


 

 

 

 

 
 
 
 
 

 

posted @ 2018-03-02 12:10  Mr-chen  阅读(165)  评论(0编辑  收藏  举报