「左程云-算法与数据结构笔记」| P10 暴力递归(上)

275 阅读5分钟

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

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


一、前缀树


前缀树,又称字典树,其实我更愿意称它为字典树,在查询的过程就类似于我们查英文字典,首先查第一个字符,然后第二个字符……,树上的每个边上都有一个字符,对于都是小写的字符串,我们就可以使用a-z,对于数组我们可以使用0-9


1、概述

  • 对于字符"abc","bcd","abe","aet","bed"
  • 它的前缀树图如下:

image.png

2、数据结构

1️⃣ 结点

	public static class TrieNode {  
	   public int path;  
	   public int end;  
	   public TrieNode[] nexts;  
	  
	   public TrieNode() {  
	      path = 0;  
	      end = 0;  
	      nexts = new TrieNode[26];  
	   }  
	}
  1. path:有多少个字符经过该结点。例如上述的红色结点,其有abc、abe两个字符经过,那么它的path=2
  2. end:有多少个字符以该结点作为结束结点。例如上述的黄色结点,只有abc一个字符串把它作为结束结点,那么它的end=0
  3. nexts:用于标识下面某个字符是否存在。例如,此处的长度为26的数组就分别标识a、b、c……,如果TrieNode[0] = 1代表着该结点的下一个有a
  4. 补充说明:此处不仅可以使用数组作为存储nexts的数据结构,也可以使用HashMap<Type,Node>用于标识Type类型的数据是否存在对应的结点。一般数据量比较大的时候就使用HashMap,因为维护多个大长度的数组太耗费空间了。

2️⃣ 前缀树

	public static class Trie {  
	   private TrieNode root;  
	   public Trie() {  
	      root = new TrieNode();  
	   }
   }
  • 其实前缀树只是封装了一个根节点作为起始节点,主要还是后面的前缀树相关的操作

3、插入操作

	public void insert(String word) {  
	   if (word == null) {  
	      return;  
	   }  
	   char[] chs = word.toCharArray();  
	   TrieNode node = root;  
	   int index = 0;  
	   for (int i = 0; i < chs.length; i++) {  
	      index = chs[i] - 'a';  
	      if (node.nexts[index] == null) {  
	         node.nexts[index] = new TrieNode();  
	      }  
	      node = node.nexts[index];  
	      node.path++;  
	   }  
	   node.end++;  
	}
  1. 首先把字符串转为字符数组word.toCharArray(),因为后面的操作是根据字符作为最小的操作单位
  2. 然后获取根节点root,进入循环,依次遍历字符数组:
    1. 首先获取该字符对应的数字index(0-25)
    2. 如果根节点的nexts中不存在这个字符,那么就新增这个字符
    3. 如果存在,那么就从根节点跳到这个结点node = root.nexts[index]
    4. 然后让这个结点的path++
    5. 然后继续遍历
  3. 循环结束后,结点是最后一个结点,让该结点的end++

4、搜索操作

	public int search(String word) {  
	   if (word == null) {  
	      return 0;  
	   }  
	   char[] chs = word.toCharArray();  
	   TrieNode node = root;  
	   int index = 0;  
	   for (int i = 0; i < chs.length; i++) {  
	      index = chs[i] - 'a';  
	      if (node.nexts[index] == null) {  
	         return 0;  
	      }  
	      node = node.nexts[index];  
	   }  
	   return node.end;  
	}
  1. 首先把字符串转换为字符数组word.toCharArray
  2. 然后同样的获取根节点,进入循环,依次遍历字符数组:
    1. 获取该字符对应的数字索引index
    2. 判断当前结点的node.nexts[index]是否存在
      1. 如果存在就进入下一个循环
      2. 如果不存在就返回0
  3. 如果遍历结束后仍然没有返回,代表找到了该字符串,返回node.end

5、删除操作

	public void delete(String word) {  
	   if (search(word) != 0) {  
	      char[] chs = word.toCharArray();  
	      TrieNode node = root;  
	      int index = 0;  
	      for (int i = 0; i < chs.length; i++) {  
	         index = chs[i] - 'a';  
	         if (--node.nexts[index].path == 0) {  
	            node.nexts[index] = null;  
	            return;  
	         }  
	         node = node.nexts[index];  
	      }  
	      node.end--;  
	   }  
	}
  1. 首先去搜索该字符串是否存在,如果不存在那就没有删除的意义
  2. 如果存在,首先把字符串转成字符数组word.toCharArray()
  3. 然后获取根节点root,进入循环,依次遍历字符数组:
    1. 首先获取该字符对应的数字索引index
    2. 让当前结点的index.nexts[index].path自减,即减少通过这个结点的路径数
    3. 如果在自减后路径数为零,就代表该路径上只有这一个字符串使用到了这个结点,直接让node.nexts[index]=null然后返回
    4. 而如果不为零,就继续遍历
  4. 直到循环结束都没有返回,就让最后一个结点的end--

6、前缀查询

	public int prefixNumber(String pre) {  
	   if (pre == null) {  
	      return 0;  
	   }  
	   char[] chs = pre.toCharArray();  
	   TrieNode node = root;  
	   int index = 0;  
	   for (int i = 0; i < chs.length; i++) {  
	      index = chs[i] - 'a';  
	      if (node.nexts[index] == null) {  
	         return 0;  
	      }  
	      node = node.nexts[index];  
	   }  
	   return node.path;  
	}
  1. 首先把我们要查询的前缀转为字符数组pre.toCharArray()
  2. 然后获取根节点root,进入循环,开始对前缀字符数组进行遍历:
    1. 搜索操作类似于上述的查询操作
    2. node.nexts[index]==null的时候就直接返回零
    3. 如果遍历结束仍然没有返回就返回最后结点的path

二、贪心算法


在某一个标准下,优先考虑最满足标准的样本,最后考虑最不满足标准的样本,最后得到一个答案的算法,叫做贪心算法。即不是找整体最优解,而是局部最优解。

1、贪心算法验证方法

  1. 实现一个不依靠贪心策略的解法X,可以用最暴力的尝试
  2. 脑补出贪心策略A、贪心策略B、贪心策略C……
  3. 用解法X和对数器去验证每一个贪心策略,用实验的方式得知哪个贪心策略正确
  4. 不要去纠结贪心策略的证明

2、最小字典序

题目:给定一个字符串类型的数组strs,找到一种拼接方式,使得把所有字符串拼起来之后形成的 字符串具有最小的字典序。

字典序:其实类似于ASCII码,即一个数字对应一个数字索引,字母越靠后索引值越大,对于一个字符串的字典序,就是排在前面的字母占更高位,类似于数字100,其中1在前面,它就占高位

实现思路

  1. 如果对数器直接比较 a.compareTo(b) 看似没有问题,实际上比如:
  2. 字符串 bba,比较字典序后发现 b 的字典序小于 ba
  3. 如果直接拼接就是 bba 实际上它的字典序小于 bab 的字典序
  4. 因此不能单纯的比较 a.compare(b)
  5. 而要比较二者两种结合方式得到的字典序的比较过后更小的那一种,即(a+b).compareTo(b+a)

实现代码

	public static class MyComparator implements Comparator<String> {  
	   @Override  
	   public int compare(String a, String b) {  
	      return (a + b).compareTo(b + a);  
	   }  
	}  
	  
	public static String lowestString(String[] strs) {  
	   if (strs == null || strs.length == 0) {  
	      return "";  
	   }  
	   Arrays.sort(strs, new MyComparator());  
	   String res = "";  
	   for (int i = 0; i < strs.length; i++) {  
	      res += strs[i];  
	   }  
	   return res;  
	}

3、会议安排

题目:一些项目要占用一个会议室宣讲,会议室不能同时容纳两个项目的宣讲。 给你每一个项目开始的时间和结束的时间(给你一个数组,里面是一个个具体的项目),你来安排宣讲的日程,要求会议室进行的宣讲的场次最多。返回这个最多的宣讲场次。

实现思路

  1. 如果以开始时间最早为标准,有可能出现下面这种情况:
    1. 如果选择时间最早的 meeting1 ,宣讲场次就是1
    2. 客观上讲,要使会议室进行的宣讲的场次最多,应该选择 meeting 2 和 meeting 3

image.png

  1. 如果以持续时间最短为标准,有可能出现下面这种情况:
    1. 如果选择持续时间最短的 meeting 1,宣讲场次就是1
    2. 客观上讲,要使会议室进行的宣讲的场次最多,应该选择 meeting 2 和 meeting 3 image.png
  2. 如果以结束时间最早为标准,那么就能安排出最佳的会议标准
    1. 实现方法:选用一个变量存储时间轴
    2. 在找到第一个结束时间最早的时间后,把结束时间赋值给该变量
    3. 后续在找到最早时间后,首先要与该时间轴比较,查看该会议开始时间是否在时间轴之后
  3. 可能上面的选择十分的无厘头,但是贪心算法,证明起来很麻烦,我们最好选择的方法就是使用对数器一个个的去比较哪一个贪心策略是正确的

实现代码

	public static class ProgramComparator implements Comparator<Program> {  
	   @Override  
	   public int compare(Program o1, Program o2) {  
	      return o1.end - o2.end;  
	   }    
	}  
	  
	public static int bestArrange(Program[] programs, int start) {  
	   Arrays.sort(programs, new ProgramComparator());  
	   int result = 0;  
	   for (int i = 0; i < programs.length; i++) {  
	      if (start <= programs[i].start) {  
	         result++;  
	         start = programs[i].end;  
	      }  
	   }  
	   return result;  
	}