「左程云-算法与数据结构笔记」| P11 补充视频(上)

295 阅读6分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第十一天,点击查看活动详情。

最近在看左神的数据结构与算法,考虑到视频讲的内容和给出的资料的PDF有些出入,不方便去复习,打算出一个左神的数据结构与算法的笔记系列供大家复习,同时也可以加深自己对于这些知识的掌握,该系列以视频的集数为分隔,今天是第九篇:P11|补充视频


一、Dijkastra算法优化


1、数据结构

1️⃣ NodeRecord

	public static class NodeRecord {  
	   public Node node;  
	   public int distance;  
	  
	   public NodeRecord(Node node, int distance) {  
	      this.node = node;  
	      this.distance = distance;  
	   }  
	}
  1. NodeRecord:用于记录一个结点到起始位置的距离
  2. node:结点
  3. distance:距离

2️⃣ NodeHeap

结点堆。由于系统实现的堆(优先级队列)只能实现插入和弹出的功能,而无法实现替换某个值,并且保持堆的结构。因此就要自己实现一个堆,用于存储结点到起始地点的距离的小根堆。


	public static class NodeHeap {  
	   private Node[] nodes;  
	   private HashMap<Node, Integer> heapIndexMap;  
	   private HashMap<Node, Integer> distanceMap;  
	   private int size;  
	  
	   public NodeHeap(int size) {  
	      nodes = new Node[size];  
	      heapIndexMap = new HashMap<>();  
	      distanceMap = new HashMap<>();  
	      this.size = 0;  
	   }
   }
  1. nodes:以数组的结构存储结点,所有的结点都存储在这个数组中
  2. heapIndexMap:而如何获取到结点,就要传入NodeheapIndexMap中获取index,然后nodes[index]就是目标结点
  3. distanceMap:以HashMap的结构存储结点到起始点的距离
  4. size:结点堆的大小,起始值为0

2、主要方法

1️⃣ 判空方法

	public boolean isEmpty() {  
	   return size == 0;  
	}
  • 这个逻辑就很简单,如果堆的大小为0,那就代表堆为空

2️⃣ 判断是否插入

	private boolean isEntered(Node node) {  
	   return heapIndexMap.containsKey(node);  
	}
  • 这个就要首先说一下 是否插入和是否在堆中 的区别:
    • 插入就是,这个数据我们有加进来做过初始化
    • 但是会存在插入过,但是后续选择的时候弹出堆的情况,那么就是不在堆中,但是还在,此时就是把距离改为-1

3️⃣ 判断是否在堆中

	private boolean inHeap(Node node) {  
	   return isEntered(node) && heapIndexMap.get(node) != -1;  
	}
  • 因为已经弹出去的数据就不需要再次计算了,所以我们要做一个备份,就表示它进入过堆,但是不在堆中

4️⃣ 插入堆化

	private void insertHeapify(Node node, int index) {  
	   while (distanceMap.get(nodes[index]) < distanceMap.get(nodes[(index - 1) / 2])) {  
	      swap(index, (index - 1) / 2);  
	      index = (index - 1) / 2;  
	   }  
	}
  • 按照我们之前堆的相关知识,修改值后应该要判断与原来的值的大小再决定与子节点还是父节点去比较,但是此处只与父节点做了比较,原因如下:
  • 调用插入堆化的情形:
    • 有更小的值替换掉原本的值,那么对于小根堆,一个更小的值就要向上做堆化
    • 插入一个新的值,由于在最后的位置,自然就要向上做堆化
  • 因此,此处的插入堆化只是做了向上的堆化

5️⃣ 堆化方法

	private void heapify(int index, int size) {  
	   int left = index * 2 + 1;  
	   while (left < size) {  
	      int smallest = left + 1 < size && distanceMap.get(nodes[left + 1]) < distanceMap.get(nodes[left])  
	            ? left + 1 : left;  
	      smallest = distanceMap.get(nodes[smallest]) < distanceMap.get(nodes[index]) ? smallest : index;  
	      if (smallest == index) {  
	         break;  
	      }  
	      swap(smallest, index);  
	      index = smallest;  
	      left = index * 2 + 1;  
	   }  
	}
  • 这个堆化是向下堆化,使用的场景是:
  • 当弹出结点,把堆顶结点与最后一个结点做交换后,头结点要向下做堆化,这个具体的代码就不剖析了,与之前堆的向下堆化大概一致

6️⃣ 弹出方法

	public NodeRecord pop() {  
	   NodeRecord nodeRecord = new NodeRecord(nodes[0], distanceMap.get(nodes[0]));  
	   swap(0, size - 1);  
	   heapIndexMap.put(nodes[size - 1], -1);  
	   distanceMap.remove(nodes[size - 1]);  
	   nodes[size - 1] = null;  
	   heapify(0, --size);  
	   return nodeRecord;  
	}
  • 弹出的时候,我们返回的数据类型是NodeRecord,用于记录结点和距离
  • 弹出小根堆的堆顶元素的流程:
    • 拿出堆顶的元素
    • 交换堆顶和最后一个元素的位置
    • 让弹出的结点的索引值设置为-1表示进过堆,但是弹出了
    • distanceMap中移除该结点的距离
    • 减少堆的大小,同时让堆的最后一个元素设置为空
    • 由于现在最后一个元素跑到了第一个元素,一定是比子节点大的,因此要做向下的堆化heapify

7️⃣ 插入方法

	public void addOrUpdateOrIgnore(Node node, int distance) {  
	   if (inHeap(node)) {  
	      distanceMap.put(node, Math.min(distanceMap.get(node), distance));  
	      insertHeapify(node, heapIndexMap.get(node));  
	   }  
	   if (!isEntered(node)) {  
	      nodes[size] = node;  
	      heapIndexMap.put(node, size);  
	      distanceMap.put(node, distance);  
	      insertHeapify(node, size++);  
	   }  
	}
  1. 首先判断是否是在堆中
    1. 如果在堆中,就在distanceMap中放入更小的那个距离
    2. 并同时做一个插入后的堆化
  2. 如果不在堆中,就看是否进入过堆
    1. 如果进入过堆就代表已经弹出过了,即不用再计算了
    2. 而如果没有进入过堆,那就要新增数据:
      1. nodes中插入node
      2. 并记录它的索引值heapIndexMap.put(node,size)
      3. 记录它的距离distanceMap.put(node,distance)
      4. 同样的做插入后的堆化insertHeapify

3、主函数

	public static HashMap<Node, Integer> dijkstra2(Node head, int size) {  
	   NodeHeap nodeHeap = new NodeHeap(size);  
	   nodeHeap.addOrUpdateOrIgnore(head, 0);  
	   HashMap<Node, Integer> result = new HashMap<>();  
	   while (!nodeHeap.isEmpty()) {  
	      NodeRecord record = nodeHeap.pop();  
	      Node cur = record.node;  
	      int distance = record.distance;  
	      for (Edge edge : cur.edges) {  
	         nodeHeap.addOrUpdateOrIgnore(edge.to, edge.weight + distance);  
	      }  
	      result.put(cur, distance);  
	   }  
	   return result;  
	}

二、汉诺塔

打印n层汉诺塔从最左边移动到最右边的全部过程

实现思路

  • 先谈谈暴力递归:
    • 暴力递归就是尝试:
    • 把问题转化为规模缩小了的同类问题的子问题
    • 有明确的不需要继续进行递归的条件,或者说退出递归的条件,防止栈溢出
    • 有当得到了子问题的结果之后的决策过程
    • 不记录每一个子问题的解
  • n层汉诺塔从最左边移到最右边的首要步骤就是把:最大的那一层从左边移动到右边,其他的移到中间
  • 要做的就是:
    • n-1层的汉诺塔放到中间
    • 第n层汉诺塔放到最右边
    • n-1层的汉诺塔放回左边
  • 然后就是:
    • n-2层的汉诺塔放到中间
    • 第n-1层汉诺塔放到最右边
    • n-2层的汉诺塔放回左边

实现代码

	public static void hanoi(int n) {  
	   if (n > 0) {  
	      func(n, n, "left", "mid", "right");  
	   }  
	}  
	  
	public static void func(int rest, int down, String from, String help, String to) {  
	   if (rest == 1) {  
	      System.out.println("move " + down + " from " + from + " to " + to);  
	   } else {  
	      func(rest - 1, down - 1, from, to, help);  
	      func(1, down, from, help, to);  
	      func(rest - 1, down - 1, help, from, to);  
	   }  
	}

三、子序列


题目:打印一个字符串的全部子序列,包括空字符串。

例如:字符串"ab"的子序列有:"a","b","ab"," "

实现思路

  • 对于一个字符串"abc",可以在分解子序列的时候的依次分情况:
    • 对于第一个位置:a 是否存在
    • 对于第二个位置:b 是否存在
    • 对于第三个位置:c 是否存在 ![[Pasted image 20221011092532.png]]
  • 这样就能列出所有情况的子序列

实现代码

	public static void function(String str) {  
	   char[] chs = str.toCharArray();  
	   process(chs, 0, new ArrayList<Character>());  
	}  
	  
	public static void process(char[] chs, int i, List<Character> res) {  
	   if(i == chs.length) {  
	      printList(res);  
	   }  
	   List<Character> resKeep = copyList(res);  
	   resKeep.add(chs[i]);  
	   process(chs, i+1, resKeep);  
	   List<Character> resNoInclude = copyList(res);  
	   process(chs, i+1, resNoInclude);  
	}
  1. 首先做判断:i 是否与字符串的长度相等。由于每次递归都会让i+1,当i与字符串长度相等的时候,就到了最后一次,直接打印即可。
  2. 如果没有到最后一次,就要进行两个递归的子过程:
    1. 一个是包括字符串的下一个字符。复制一个原本的集合,加入字符串这个字符
    2. 一个是不包括字符串的下个字符。复制一个原本的集合,i+1,但是不加入这个字符

四、字符串全排列


题目:打印一个字符串的全部排列,要求不要出现重复的排列

例如:字符串"abc"的全部排列就有:"abc","acb","bac","bca","cab","cba"

实现思路

  • 对于一个字符串"abc",可以按位置一个个分情况:
    • 当第一个位置是a,那么第二三两个位置就是:"bc","cb"
    • 当第一个位置是b,那么第二三两个位置就是:"ac","ca"
    • 当第一个位置是c,那么第二三两个位置就是:"ab","ba"
    • ![[Pasted image 20221011094158.png]]

实现代码

	public static ArrayList<String> Permutation(String str) {  
	   ArrayList<String> res = new ArrayList<>();  
	   if (str == null || str.length() == 0) {  
	      return res;  
	   }  
	   char[] chs = str.toCharArray();  
	   process(chs, 0, res);  
	   res.sort(null);  
	   return res;  
	}  
	  
	public static void process(char[] chs, int i, ArrayList<String> res) {  
	   if (i == chs.length) {  
	      res.add(String.valueOf(chs));  
	   }  
	   boolean[] visit = new boolean[26];  
	   for (int j = i; j < chs.length; j++) {  
	      if (!visit[chs[j] - 'a']) {  
	         visit[chs[j] - 'a'] = true;  
	         swap(chs, i, j);  
	         process(chs, i + 1, res);  
	         swap(chs, i, j);  
	      }  
	   }  
	}
  1. 此处没有做打印,是把数据放进结果集中返回
  2. 然后此处可能不是很好理解,这里拿出主题代码做单独分析
    1. swap(chs,i,j)就是在for循环中依次把字符数组中后面的数据放在第一个
    2. process(chs,i+1,res)就是递归调用,依次把字符数组中后面的数据放在第二个
    3. swap(chs,i,j)就是在进行调用结束后,为了不使用额外的空间,把数据恢复为原本的模样
  3. 上述中boolean[] visit是为了去重,如果该字符试过了,就在这个数组中去标识该字符已经试过了