【左程云 数据结构与算法笔记】P10 暴力递归

238 阅读3分钟

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

下面是我整理的跟着b站左程云的数据结构与算法学习笔记,欢迎大家一起学习。

前缀树

不以点以边更好 虽然也可以以点表示值,但总体效果不如边,比如头节点的展示 包括经典的算法等都是基于边 节点代码

public static class TrieNode {  
  
    //有多少点经过这里  
    public int path;  
    //有多少点依此为终点  
    public int end;  
    public TrieNode[] nexts;  
  
    public TrieNode() {  
        path = 0;  
        end = 0;  
  //26代表底下可能可以连26个字母 如nexts[0]!=null 该字母有通向a的路  
        nexts = new TrieNode[26];  
    }  
}

当有线经过该点时,p++,如果是尾节点,e++ 如 当为a时,a两边的p++; 若e=1;则有到该点的边 把前缀树建好,代价是字符的数量 若想知道有多少字符串以ab为前缀 可以看ab结点上的p值 构建前缀树

将字符串构造进前缀树中

准备一个公共的头节点root,根节点的pass++;表示加入多少个字符串 先将字符串转为字符数组,通过遍历字符数组,将字符转化为数字,即字符串-'a'得到a=0... 若指定节点的node[index]为空,新建节点,节点继续走到node[index];并将path++;将最后一个遍历的node节点上的end++ 代码实现

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++;  
}

26代表26个小写字母,若字符种类过多,可将其转化为HashMap<Char,Node> 某个字符的下一个节点 路与路之间有序组织可以用TreeMap

查找指定字符串

思路:若已经插入abc

  • 查找ab,当ab长度走完时,停留在b之后的节点上,该节点上的end值为0,不存在该字符串
  • 查找abcd,此时长度遍历到c后的边时,node下没有该值,返回空,不存在该值
  • 若为abc,返回c后节点下的end值 代码实现
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;  
}

查找有多少个字符以指定字符串为前缀

思路:代码与查找指定字符串相似,只是遍历到该节点后,取出pass值 代码实现

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;  
}

如图,在前缀树中删除abe 跟着abe一个一个节点遍历,沿途p--;最后的节点e-- 代码实现

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--;  
    }  
}

贪心算法

经典应用

可以先按开会时间最早进行安排 但是效率不低,若为开会结束时间最早进行安排,可以得到最优解 贪心算法可以通过举反例的方式淘汰一些想法

以开会结束时间最早进行安排

思路实现:将所有的开会结束时间进行排序,得到结束时间从早到晚的排序,如果开始时间大于可以开始的时间点,则安排,并将时间点设置为之前安排会议的结束时间 流程图 代码实现

public static class Program {  
    public int start;  
    public int end;  
  
    public Program(int start, int end) {  
        this.start = start;  
        this.end = end;  
    }  
}  
  
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;  
}

解题套路 将所有字符串中字符串选择拼接顺序,形成最小字典序的结果 若两个字母长度不一,将短的字母用0补齐长度,按照字母顺序由高位到地位排序 但这种方案并不能保证字典序最低 如图字典序最低为bab 新的贪心策略 加上比较 若a,b 若a,b<=b,a 则a前,否则b前 可以理解为两个数的相加 a,b=a* k^b+b 证明过程是一个不断进行数学运算的过程 证明出比较策略具有传递性,可以排出唯一的一组策略 前,后<=后,前 若a,b之间有其他字母 用数学归纳法可以证明不知相邻都能推出该结论,进而证明贪心算法有序

典型例题

例题1

相当于 求最小的整体和 解题思路 使用小根堆,通过不断将最小的两值进行合并并重新放进去,反过来实现该工程

得到沿途最低代价

public static int lessMoney(int[] arr) {  
    PriorityQueue<Integer> pQ = new PriorityQueue<>();  
    for (int i = 0; i < arr.length; i++) {  
        pQ.add(arr[i]);  
    }  
    int sum = 0;  
    int cur = 0;  
    while (pQ.size() > 1) {  
        cur = pQ.poll() + pQ.poll();  
        sum += cur;  
        pQ.add(cur);  
    }  
    return sum;  
}

例题2

通过不断的投入,获得收益,启动更大的项目 只是每一步都没有选择,且最大利润只能到7,下面这种情况更能说明贪心算法 解题思路 根据花费加进小根堆,并将所有项目加锁,通过成本找出<=成本的项目,解锁该项目,并将其通过利润加进大根堆,将弹出的项目(即为最大收益项目)进行,并获得收益,循环反复

在数据流中,随时取得中位数

实现步骤: 准备一个大根堆和一个小根堆

  1. 将第一个数字放进大根堆
  2. 判断第二个数是否小于大根堆堆顶,若是,入大根堆,否则进小根堆
  3. 判断大小根堆的大小,如果大size-小size>=2,较大堆顶弹出进较小的堆 如以下的流程
  • 5先进入大根堆,3小于5进大根堆,此时大根堆的长度超过小根堆的长度超过2,将5弹出并放进小根堆;
  • 7和4都大于3放进小根堆,此时小根堆的长度超过大根堆的长度超过2,将4弹出并放进大根堆 此时较小的中位数在大根堆中,较大的中位数在小根堆中 整个流程的所有调整都为logN水平

N皇后问题

isvalid函数的过程 n行各有n种选择 时间复杂度n^n 代码实现

public static int num1(int n) {  
    if (n < 1) {  
        return 0;  
    }  
    int[] record = new int[n];  
    return process1(0, record, n);  
}  
// 目前来到了第i行,record[0...i-1]之前的行放过的皇后 整体一共有n行 返回值为合理的摆法  
public static int process1(int i, int[] record, int n) {  
    //终止行  
    if (i == n) {  
        return 1;  
    }  
    int res = 0;  
    for (int j = 0; j < n; j++) {  
        if (isValid(record, i, j)) {  
            record[i] = j;  
            res += process1(i + 1, record, n);  
        }  
    }  
    return res;  
}  
  
public static boolean isValid(int[] record, int i, int j) {  
    for (int k = 0; k < i; k++) {  
        // Math.abs(record[k] - j) == Math.abs(i - k)判断两点是否共斜线 即45度  
        if (j == record[k] || Math.abs(record[k] - j) == Math.abs(i - k)) {  
            return false;  
        }  
    }  
    return true;  
}

常数优化,运用位运算

 //常数优化版本 通过位运算,最好不要超过32位

	public static int num2(int n) {
		if (n < 1 || n > 32) {
			return 0;
		}
		int upperLim = n == 32 ? -1 : (1 << n) - 1;
		return process2(upperLim, 0, 0, 0);
	}

	public static int process2(int upperLim, int colLim, int leftDiaLim,
			int rightDiaLim) {
		if (colLim == upperLim) {
			return 1;
		}
		int pos = 0;
		int mostRightOne = 0;
		pos = upperLim & (~(colLim | leftDiaLim | rightDiaLim));
		int res = 0;
		while (pos != 0) {
			mostRightOne = pos & (~pos + 1);
			pos = pos - mostRightOne;
			res += process2(upperLim, colLim | mostRightOne,
					(leftDiaLim | mostRightOne) << 1,
					(rightDiaLim | mostRightOne) >>> 1);
		}
		return res;
	}

位的限制主要通过求出左限制,右限制与前一行并将这三个或求和 下一轮中,左限制左移,右限制右移 位置的限制通过移位很容易得到