左神bilibili算法笔记-基础提升

哈希函数与哈希表

哈希函数

基本概念:
  • 输⼊域是趋于⽆穷的,输出域是有界的(S域)
  • 确定性:⼀个哈希函数对于相同的输入,⼀定返回相同的结果(说明哈希函数内部没有随机过程)
  • 哈希碰撞:由于输出域有限⽽输⼊域⽆穷,不同的输⼊有可能产⽣相同的结果(但概率极⼩)
  • 均匀性:输出域任⼀等⾯积区域内,样本的个数几乎相同==(同时做到离散性和均匀性)==!!!特别重要!!! ⼀个输出均匀分布的哈希函数,对输出进⾏%m取余运算,则结果在0~m-1上也均匀分布
常见应用:
  • MDS算法
  • SHa1算法
哈希表的实现:
  • 基本逻辑:哈希函数计算⼀个哈希值,哈希值进行%m,同⼀位置放⼊⼀个单向链表,⼀个结点放⼀个数和出现次数, 当链表长度超过设定阈值时,扩容(如m翻倍) N个数放⼊哈希表中时,最多扩容O(logN)次,每次代价为O(N),平摊到每个数上,⼀个数放⼊的时间复杂度为O(logN)

40亿整数中出现最多的

题⽬:40亿(0~2^32 -1)个⽆符号整数,只给1G空间,要求给出出现次数最多的数

传统⽅法⽤HashMap来解题,但这⾥会超过1G,不可取

思路:⽤哈希函数得到⼀个映射,再对映射结果%100,则分成100个⼩⽂件,相同的数⼀定在同⼀个⽂件中,每个⽂件中都有⼀个数出现次数最多,接下来再⽤哈希表统计每个⽂件⾥数据的词频,再各⽂件词频最⾼的作⽐较,看哪个数据个数最多

RandomPool结构

题⽬:设计⼀种结构,在该结构中有如下三个功能:

insert(key):将某个key加⼊到该结构,做到不重复加⼊

delete(key):将原本在结构中的某个key移除

getRandom(): 等概率随机返回结构中的任何⼀个key。

要求:Insert、delete和getRandom⽅法的时间复杂度都是O(1) 思路:连续区间

public class RandomPool<K> {
    private HashMap<K, Integer> keyIndexMap;
    private HashMap<Integer, K> indexKeyMap;
    private int size;

    public RandomPool() {
        keyIndexMap = new HashMap<>();
        indexKeyMap = new HashMap<>();
        size = 0;
    }

    public void insert(K key) {
        if (!keyIndexMap.containsKey(key)) {
            keyIndexMap.put(key, size);
            indexKeyMap.put(size++, key);
        }
    }

    public void delete(K key) {
        if (keyIndexMap.containsKey(key)) {
            Integer deleteIndex = keyIndexMap.get(key);
            int lastIndex = --size;
            K lastKey = indexKeyMap.get(lastIndex);
            keyIndexMap.put(lastKey, deleteIndex);
            indexKeyMap.put(deleteIndex, lastKey);
            keyIndexMap.remove(key);
            indexKeyMap.remove(lastIndex);
        }
    }

    public K getRandom() {
        if (this.size == 0) {
            return null;
        }
        // 0 ~ size -1
        int randomIndex = (int) (Math.random() * this.size); 
        return this.indexKeyMap.get(randomIndex);
    }

}

布隆过滤器

  • 去重、构造⿊名单之类的事情

  • 没有删除⾏为,只要加⼊查询两个⾏为,使⽤空间少

  • 允许⼀定失误率:把不在容器⾥的当成在容器⾥的了

位图

如果用Hash表存记录会需要很大的存储空间,例如一个URL是64B,存100亿个就需要640G内存,由此引出位图,注意使用位图实现布隆过滤器失误不可避免

⼀个数组,每个位置只有⼀个bit,相当于int的类型的1/32

  • ⽤两个下标来找⼀个地⽅:⽐如要找第178位

    • int[]arr⾥的第numIndex个数字上找:numIndex = 178 / 32;

    • 去这个数字的第bitIndex位上找:bitIndex = 178 % 32;

  • 如:第178位: 查:int s = ((arr[numIndex] >> (bitIndex))& 1);

    • 查:int s = ((arr[ i / 32 ] >> ( i % 32 ))& 1);

    • 改成1:arr[numIndex] = arr[numIndex] | (1 << (bitIndex))

    • 改成0:arr[numIndex] = arr[numIndex] & (~(1 << (bitIndex)))

public class BitMap {

    public static void main(String[] args) {
        //32bit * 10 = 320 bit
        //arr[0] int 0 ~ 31
        //arr[1] int 32 ~ 63
        int[] arr = new int[10];

        //要找第178位
        int i = 178;

        int numIndex = 178 / 32;
        int bitIndex = 178 % 32;
        //拿到178位状态
        int s = (arr[numIndex] >> bitIndex) & 1;

        //178位改成1
        arr[numIndex] = arr[numIndex] | (1 << bitIndex);

        //178位改成0
        arr[numIndex] = arr[numIndex] & (~(1 << bitIndex));
    }
}
布隆过滤器实现

建⽴⼀个位图:bit[m]

加⼊数据:

将每个数据放⼊ k 个哈希函数,得到 k 个哈希值每个哈希值都 %m,得到的结果对应的 bit[] 位置置为1

查找数据在不在⾥⾯:

将⼀个变量放⼊ k 个哈希函数,得到 k 个哈希值,每个哈希值都 %m, 得到的结果和 bit[] 对应位置⽐较,如果全是1,那就是在⿊名单⾥

随着m的增加,失误率下降,对于确定的m,存在唯⼀的k使P最⼩

设计布隆过滤器:

条件:样本量N、失误率P

要求出的结果:m和k

image-20220916000216301

⼀致性哈希原理

多服务器数据组织⽅法

分布式数据库,关于底层数据服务器怎么组织

⼀个数据 ----哈希函数----> 哈希值 ----%m----> a,放到m个服务器中的第a个⾥

⾼频、中频、低频数据如果都很多的话,平均分配在m个服务器上

哈希key的选择:⼀定要选择种类⽐较多,让⾼频、中频、低频的数负载均衡起来

哈希环技术

⽤上述⽅法,当机器数⽬从m变成了p个,那么数据迁移的代价是全量的

解决⽅法:

  • 将 S 域变成⼀个圈(0和最⼤值⾸尾相连),m个服务器所对应的哈希值在圈上均匀分布

  • 每当放⼊⼀个新的数字时,计算哈希值,在圈上找顺时针离得最近的数字

增加、减少⼀个服务器的时候,数据迁移的代价很少

⼀些问题:

  • 机器较少时,均分较难 即便均分,

  • 在增减机器时,也会破坏均匀性

虚拟节点技术

⽤来解决上述的两个问题:

每个机器1000个虚拟结点,计算哈希值放在哈希环上

  • 环上任意⼀端等长度的区间⾥,可以认为属于各机器的结点数⽬相同

  • 由此可以解决均分问题,以及增减问题

同时,可以根据机器性能分配不⼀样多的结点个数

并查集

岛问题

⼀个问题引出并查集

题⽬:⼀个矩阵中只有0和1两种值,每个位置都可以和⾃⼰的上、下、左、右 四个位置相连,如果有⼀⽚1连在⼀起,这个部分叫做⼀个岛,求⼀个矩阵中有多少个岛?

举例:输⼊:

0 0 1 0 1 0

1 1 1 0 1 0

1 0 0 1 0 0

0 0 0 0 0 0

返回:有3个岛

思路:infect – 从⼀个1出发,将相连的所有1都变成2

public class Lands {

    @Test
    public void test() {
        System.out.println(countIslands(new int[][]{{0, 0, 1, 0, 1, 0}, {1, 1, 1, 0, 1, 0}, {1, 0, 0, 1, 0, 0}, {0, 0, 0, 0, 0, 0}}));
    }

    public int countIslands(int[][] m) {
        if (m == null || m[0] == null) {
            return 0;
        }
        int res = 0;
        for (int i = 0; i < m.length; i++) {
            for (int j = 0; j < m[0].length; j++) {
                if (m[i][j] == 1) {
                    res++;
                    // 对这个岛进⾏感染
                    infect(m, i, j);
                }
            }
        }
        return res;
    }

    private void infect(int[][] m, int i, int j) {
        if (i > m.length || i < 0 || j > m[0].length || j < 0 || m[i][j] != 1) {
            return;
        }
        // 把这个位置上的数字改成 2
        m[i][j] = 2;
        // 这个位置下侧进⾏感染
        infect(m, i + 1, j);
        // 这个位置右侧进⾏感染
        infect(m, i, j + 1);
        // 这个位置上侧进⾏感染
        infect(m, i - 1, j);
        // 这个位置左侧进⾏感染
        infect(m, i, j - 1);
    }
}

时间复杂度:O(N×M) 如何并⾏算法来解决这个问题,如何⽤多核系统解决这个问题? – 并查集

并查集实现

成员:⼀个集合,存放⼀个或多个数据

查询操作:查询两个数据是否属于统⼀集合

合并操作:把a所在的集合和b所在的集合合并

要求两个操作都是O(1) ,传统的如链表拼接Hash存集合都有一个操作不符合O(1)

实现⽅法:

  • ⽤向上指的图查询时,两个元素都往上指,指到不能再指,若此时俩⼀样就是⼀个集合,否则不是

  • 合并时,把数量⼩的顶指向数量多的顶

public class UnionFind {

    class Element<V> {
        public V value;

        public Element(V value) {
            this.value = value;
        }
    }

    class UnionFindSet<V> {
        // 值到结点的映射
        public Map<V, Element<V>> elementMap;
        // key - 元素;value - 该元素的⽗
        public Map<Element<V>, Element<V>> fatherMap;
        // key - 某集合代表元素 value - 集合⼤⼩
        public Map<Element<V>, Integer> rankMap;

        public UnionFindSet(List<V> list) {
            // 初始化,⼀个列表⾥,每个元素都单独⼀个集合
            elementMap = new HashMap<>();
            fatherMap = new HashMap<>();
            rankMap = new HashMap<>();
            for (V value : list) {
                elementMap.put(value, new Element<>(value));
                Element<V> element = elementMap.get(value);
                fatherMap.put(element, element);
                rankMap.put(element, 1);
            }
        }

        private Element<V> findHead(Element<V> element) {
            Stack<Element<V>> stack = new Stack<>();
            while (element != fatherMap.get(element)) {
                stack.push(element);
                element = fatherMap.get(element);
            }
            while (!stack.isEmpty()) {
                // 把路径的⽗亲都改成“头”
                fatherMap.put(stack.pop(), element);
            }
            return element;
        }

        public boolean isSameSet(V a, V b) {
            if (elementMap.containsKey(a) && elementMap.containsKey(b)) {
                Element<V> aEle = elementMap.get(a);
                Element<V> bEle = elementMap.get(b);
                return findHead(aEle) == findHead(bEle);
            }
            return false;
        }

        public void union(V a, V b) {
            if (elementMap.containsKey(a) && elementMap.containsKey(b)) {
                Element<V> aHead = findHead(elementMap.get(a));
                Element<V> bHead = findHead(elementMap.get(b));
                // 哪个集合⼤
                Element<V> big = rankMap.get(aHead) > rankMap.get(bHead) ? aHead : bHead;
                // 哪个集合⼩
                Element<V> small = big == aHead ? bHead : aHead;
                fatherMap.put(small, big);
                // 修改big的⼤⼩
                rankMap.put(big, rankMap.get(big) + rankMap.get(small));
                // 删掉small结点
                rankMap.remove(small);
            }
        }
    }
}

岛问题可以分为多个模块用多个CPU并行处理,处理完成后用并查集判断边界

KMP算法

问题:字符串str1和str2,str1是否包含str2,如果包含返回str2在str1中开始的位置。

如何做到时间复杂度==O(N)==完成?

暴⼒⽅法:O(N×M)

KMP基本概念

⼀个str2 = abbabbk,k下⾯要对应⼀个数,这个数来⾃abbabb

前缀和后缀相等的最⼤长度:如abbabb = 3

12345
aababbabbaabbab
bbbabbbabbbbabb

next数组

arr = [ a, a, b, a, a, b, s, a, a, b, a, a, b, t]

next = [-1, 0, 1, 0, 1, 2, 3, 0, 1, 2, 3, 4, 5, 6]

第⼀位填-1,第⼆位填0

后⾯每个位置前⾯【所有字符形成的字串】的【前缀和后缀相等的最⼤长度】填在next数组的这个位置上

具体过程

情况:

当 str2 的第 0 位来到了 str1 第 i 位

且 str2 的第 0~p-1位都匹配上了,但第 p 位和 str1的第 i+p 位匹配不上

操作:

此时看 next 数组的第 p 位的值为 m,

将 str2 的第 0 位移动到 str1 第 i+p-m 位

⽐较 str2 的第 m+1 位是不是等于str1 的第 i+p 位

举例说明:

如:abbstkscabbstks

和:abbstkscabbstkz

此时,str2 的 0 位置来到了 str1 的第 i 位,它们的末尾不⼀样,但 next 数组在 z 位置处的值为 6

表⽰,str2 的前 6 个字母 abbstk 和 str1的 s 前的最后 6 个字母 abbstk 是匹配的

故直接检查 str2 第 7 个字母是不是等于 s ,即 str2 开头的 a 直接和 str1 第 i+8 位的 a 对齐

public class KMP {

    @Test
    public void test() {
        String str1 = "shfjhhjhhjdfgsdgdsgsdgdsgsdsaknnjslnnal";
        String str2 = "knnjs";
        System.out.println(getIndexOf(str1, str2));
    }

    /**
     * @param s 长串
     * @param m 短串
     * @return
     */
    public int getIndexOf(String s, String m) {
        if (s == null || m == null || m.length() < 1 || s.length() < m.length()) {
            return -1;
        }
        // 两个字符串都变成-字符数组
        char[] str1 = s.toCharArray();
        char[] str2 = m.toCharArray();
        // 得到m串的next数组
        int[] next = getNextArray(str2);
        int i1 = 0;
        int i2 = 0;
        while (i1 < str1.length && i2 < str2.length) {
            // 对应位相等则⽐较下⼀位
            if (str1[i1] == str2[i2]) {
                i1++;
                i2++;
                // 这两位不同 且 str2还在第1位,那就str2向后⾛
            } else if (next[i2] == -1) {
                i1++;
                // 这两位不同 且 str2不在第⼀位,那i2移动到next[i2]位
            } else {
                i2 = next[i2];
            }
        }
        return i2 == str2.length ? i1 - i2 : -1;
    }

    /**
     * 得到ms串的next数组
     *
     * @param ms
     * @return
     */
    public int[] getNextArray(char[] ms) {
        if (ms.length == 1) {
            return new int[]{-1};
        }
        int[] next = new int[ms.length];
        next[0] = -1;
        next[1] = 0;
        // 从第三位开始
        int i = 2;
        int cn = 0;
        while (i < next.length) {
            // 这⼀步结束以后,i移到了i+1,cn的值为next[i],每⼀步都假设前⼀步是对的
            if (ms[i - 1] == ms[cn]) {
                next[i++] = ++cn;
            } else if (cn > 0) {
                cn = next[cn];
            }else {
                next[i++] = 0;
            }
        }
        return next;
    }
}

0位置是-1,1位置是0

如果0位置和1位置相等,那1位置就是1,否则是0

第i位的借助第i-1位的值:

  • 如果str[ i - 1 ] == str2[ next[ i - 1 ] ],那next[ i ] = next[ i - 1 ] + 1,不可能更长
  • 如果不相等,则⽐较str[ i - 1] == str[ next[ next[ i - 1 ] ] ],只要相等,那next[ i ]就等于右边⽅括号⾥的值+1
  • 如果已经到了0位置还不相等,那 next[ i ] = 0

Manacher算法

⽬标问题:

  • 字符串str中,最长回⽂⼦串的长度如何求解?
  • 如何做到时间复杂度O(N)完成?

如:abc1232ode1,其最⼤回⽂⼦串为232,长度为3

暴⼒解:遍历,以⼀个数为中⼼向两边扩。

⼀⽅⾯时间复杂度⾼O(N ),另⼀⽅⾯,偶数长度的回⽂字符串会被跳过

在了两个字符之间插⼊#,如 #1#2#2#1…

基本概念

回⽂直径

回⽂半径

回⽂半径数组

之前所扩的所有位置中 所到达的最右回⽂右边界 R,对称点 L

取得这个边界的时候,中⼼点在哪 C

C是伴随R改变⽽改变的,如果R不变,C也不变

具体过程

  1. 当前点不在回⽂右边界⾥⾯,暴⼒阔

  2. 当前点在回⽂右边界⾥,即 L < i’ < C < i < R

    1. i’ 的回⽂区域在[L,R]中 => i 的回⽂区域也在[L,R]中,长度与 i’ 相同
    2. i’ 的回⽂有⼀部分在区域在[L,R]外 => i 的半径只有 i 到 R
    3. i’ 的回⽂左边界正好是L,i 的半径有可能更⼤,从 R 外的第⼀个字符暴⼒扩,得到 i 的半径

为了优化代码,就不区分需不需要往右扩的情况了,不需要往右扩的最多判断多一次,不会影响时间复杂度

public class Manacher {

    @Test
    public void test() {
        System.out.println(maxLcpsLength("asfxxxbasbsdssabsb"));
    }

    /**
     * 返回最大回文长度
     *
     * @param str
     * @return
     */
    public int maxLcpsLength(String str) {
        if (str == null || str.length() == 0) {
            return 0;
        }
        char[] chars = manacherString(str);
        //各点回文长度数组
        int[] pArr = new int[chars.length];
        //中点位置
        int c = -1;
        //右边界的下⼀个位置
        int r = -1;
        //返回值
        int max = Integer.MIN_VALUE;
        for (int i = 0; i < chars.length; i++) {
            pArr[i] = r > i ? Math.min(r - i, c * 2 - i) : 1;
            while (i + pArr[i] < chars.length && i - pArr[i] > -1) {
                // 左边界往左⼀个和右边界向右加⼀个⽐较
                if (chars[i + pArr[i]] == chars[i - pArr[i]]) {
                    // 相等就扩1
                    pArr[i]++;
                }else {
                    // 不相等就进入下一个i判断
                    break;
                }
            }
            if (i + pArr[i] > r) {
                r = i + pArr[i];
                c = i;
            }
            max = Math.max(max, pArr[i]);
        }
        return max;
    }

    /**
     * 在字符串每个字符中间插⼊ #
     *
     * @param str
     * @return
     */
    public char[] manacherString(String str) {
        char[] chars = str.toCharArray();
        char[] res = new char[chars.length * 2 + 1];
        int index = 0;
        for (int i = 0; i < res.length; i++) {
            res[i] = (i & 1) == 0 ? '#' : chars[index++];
        }
        return res;
    }

}

滑动窗⼝和单调栈

滑动窗⼝

由⼀道题引出滑动窗⼝⽅法

题⽬:有⼀个整型数组arr和⼀个⼤⼩为w的窗⼝从数组的最左边滑到最右边,窗⼝每次向右边滑⼀个位置。

例如,数组为[4,3,5,4,3,3,6,7],窗⼝⼤⼩为3时:

[4 3 5] 4 3 3 6 7

4 [3 5 4] 3 3 6 7

4 3 [5 4 3] 3 6 7

4 3 5 [4 3 3] 6 7

4 3 5 4 [3 3 6] 7

4 3 5 4 3 [3 6 7]

如果数组长度为n,窗⼝⼤⼩为w,则⼀共产⽣n-w+1个窗⼝的最⼤值。

请实现⼀个函数。 输⼊:整型数组arr,窗⼝⼤⼩为w。

输出:⼀个长度为n-w+1的数组res,res[i]表⽰每⼀种窗⼝状态下的 以本题为例,结果应该返回{5,5,5,4,6,7}。

public class WindowMax {
    @Test
    public void test() {
        System.out.println(Arrays.toString(getMaxWindow(new int[]{4, 3, 5, 4, 3, 3, 6, 7}, 3)));
    }

    /**
     *窗⼝最⼤值题解
     * @param arr
     * @param w
     * @return
     */
    public int[] getMaxWindow(int[] arr, int w) {
        if (arr == null || arr.length < w) {
            return null;
        }
        LinkedList<Integer> qMax = new LinkedList<>();
        int[] res = new int[arr.length - w + 1];
        int index = 0;
        for (int i = 0; i < arr.length; i++) {
            // 右端扩充
            while (!qMax.isEmpty() && arr[qMax.peekLast()] <= arr[i]) {
                qMax.pollLast();
            }
            qMax.addLast(i);
            // 最⼤值如果是左端第⼀个,那弹出
            if (qMax.peekFirst() == i - w) {
                qMax.pollFirst();
            }
            if (i >= w - 1) {
                res[index++] = arr[qMax.peekFirst()];
            }
        }
        return res;
    }
}

滑动窗⼝基本结构

这⾥以最⼤值滑动窗⼝为例

  • 窗⼝的左边界和右边界最开始都在最左边

  • 左边界和右边界都只能往右⾛

  • 左边界不能超过右边界

使⽤双端队列 – 两边都可以进出

在双端队列⾥放下标

确保⾥⾯的下标对应的数据是从⼤到⼩的

  • 头部值最⼤

  • 只能从尾部进⼊

    • 如果尾部进⼊不能保证头->尾是单调递减,那就依次弹出,直到满⾜单调递减

    • 弹出的数据不找回

L 往右动时,如果双端队列头部是 L 的前⼀个,那把这个数据从头部弹出,

  • 不是就不⽤管

实质:双端队列⾥⾯维护的信息是:如果 R 不动,L 动的情况下,谁会依次成为最⼤值,这个优先级信息

每个位置的数,最多进队列⼀次,出队列⼀次

public class WindowMax {

    private int L;
    private int R;
    private int[] arr;
    private LinkedList<Integer> queue;

    public WindowMax(int[] arr) {
        L = -1;
        R = 0;
        this.arr = arr;
        queue = new LinkedList<>();
    }

    public void addNumFromRight() {
        if (R == arr.length) {
            return;
        }
        while (!queue.isEmpty() && arr[queue.pollLast()] <= arr[R]) {
            queue.pollLast();
        }
        queue.addLast(R);
        R++;
    }

    public void removeNumFromLeft() {
        if (L >= R - 1) {
            return;
        }
        L++;
        //有对应下标的值才需要弹出,对应下标可以在addNumFromRight时已经弹出了
        if (queue.peekFirst() == L) {
            queue.pollLast();
        }
    }

    public Integer getMax() {
        if (!queue.isEmpty()) {
            return arr[queue.getFirst()];
        }
        return null;
    }
}

单调栈

要解决的问题:

在数组中想找到⼀个数,左边和右边⽐这个数⼤、且离这个数最近的位置。

如果对每⼀个数都想求这样的信息,能不能整体代价达到O(N)?需要使⽤到单调栈结构

基本结构和流程

建⽴⼀个栈,放的是数据的下标,从底到顶数据从⼤到⼩

流程:

  • 挨个往⾥放,确保下⾯的更⼤

  • 如果进来的这个数,让栈不能继续是下⼤上⼩,那弹出⼀个数

    • 弹出这个数左边较⼤的就是它下⾯的数

    • 弹出这个数右边较⼤的就是让它弹出的那个数(刚刚要进来但是太⼤)

  • 最后是清算阶段(栈⾥有东西):

    • 依次弹出⾥⾯的数,它们都没有右侧较⼤的数

    • 弹出这个数左边最⼤的就是它下⾯的数

每个数据只进栈⼀次,出栈⼀次以上是⽆重复值的情况,若有重复值,则:

  • 将栈内存放的数据类型改成链表,相同⼤⼩的值压在⼀起
  • 弹出⼀个数时,左边最近的较⼤值为:下⽅链表的尾端值

两侧最近的较⼩数

在数组中想找到⼀个数,左边和右边⽐这个数⼩、且离这个数最近的位置。

如果对每⼀个数都想求这样的信息,能不能整体代价达到O(N)?需要使⽤到单调栈结构

分有重复数字和⽆重复数字的情况

⽆重复数字:

public class MonotonousStack {
    @Test
    public void test() {
        int[][] nearLessNoRepeat = getNearLessNoRepeat(new int[]{8, 9, 23, 4, 3, 58, 30, 5});
        for (int[] ints : nearLessNoRepeat) {
            System.out.println(Arrays.toString(ints));

        }
    }

    public int[][] getNearLessNoRepeat(int[] arr) {
        int[][] res = new int[arr.length][2];
        Stack<Integer> stack = new Stack<>();
        for (int i = 0; i < arr.length; i++) {
            while (!stack.isEmpty() && arr[stack.peek()] > arr[i]) {
                Integer pop = stack.pop();
                int leftLessIndex = stack.isEmpty() ? -1 : stack.peek();
                res[pop][0] = leftLessIndex;
                res[pop][1] = i;
            }
            stack.push(i);
        }
        // 清算阶段
        while (!stack.isEmpty()) {
            Integer pop = stack.pop();
            int leftLessIndex = stack.isEmpty() ? -1 : stack.peek();
            res[pop][0] = leftLessIndex;
            res[pop][1] = -1;
        }
        return res;
    }
}

有重复数字:

public class MonotonousStack {
    public int[][] getNearLess(int[] arr) {
        int[][] res = new int[arr.length][2];
        Stack<List<Integer>> stack = new Stack<>();
        for (int i = 0; i < arr.length; i++) {
            while (!stack.isEmpty() && arr[stack.peek().get(0)] > arr[i]) {
                List<Integer> pop = stack.pop();
                // 取位于下⾯位置的列表中,最晚加⼊的那个
                int leftLessIndex = stack.isEmpty() ? -1 : stack.peek().get(stack.peek().size() - 1);
                for (Integer integer : pop) {
                    res[integer][0] = leftLessIndex;
                    res[integer][1] = i;
                }
            }
            if (!stack.isEmpty() && stack.peek().get(0) == arr[i]) {
                stack.peek().add(i);
            } else {
                ArrayList<Integer> list = new ArrayList<>();
                list.add(i);
                stack.push(list);
            }
        }
        // 清算阶段
        while (!stack.isEmpty()) {
            List<Integer> pop = stack.pop();
            int leftLessIndex = stack.isEmpty() ? -1 : stack.peek().get(stack.peek().size() - 1);
            for (Integer integer : pop) {
                res[integer][0] = leftLessIndex;
                res[integer][1] = -1;
            }
        }
        return res;
    }
}

(累计和×最⼩值)最⼤的⼦数组

题⽬:定义:数组中累积和与最⼩值的乘积,假设叫做指标A。

给定⼀个正数数组,请返回⼦数组中,指标A最⼤的值。

暴力解,时间复杂度O(N^2)

public class MaxProduct {

    public static int max1(int[] arr) { // 暴⼒解
        int max = Integer.MIN_VALUE;
        // 对左区间遍历
        for (int i = 0; i < arr.length; i++) {
            // 对右区间遍历
            for (int j = i; j < arr.length; j++) {
                int minNum = Integer.MAX_VALUE;
                int sum = 0;
                for (int k = i; k <= j; k++) {
                    // 区间数字累加求和
                    sum += arr[k];
                    // 区间最⼩值
                    minNum = Math.min(minNum, arr[k]);
                }
                // 更新最⼤的(min*sum)
                max = Math.max(max, minNum * sum);
            }
        }
        return max;
    }
}

单调栈:

public class MaxProduct {

    @Test
    public void test() {
        System.out.println(max2(new int[]{4, 9}));
    }

    /**
     * 单调栈解法
     *
     * @param arr
     * @return
     */
    public int max2(int[] arr) {
        // sums[i]表⽰arr[0]~arr[i]的和
        int[] sum = new int[arr.length];
        sum[0] = arr[0];
        for (int i = 1; i < sum.length; i++) {
            sum[i] = sum[i - 1] + arr[i];
        }

        Stack<Integer> stack = new Stack<>();
        int max = Integer.MIN_VALUE;
        for (int i = 0; i < sum.length; i++) {
            // 栈不空 且 栈顶对应的数⽐arr[i]⼤
            while (!stack.isEmpty() && arr[stack.peek()] >= arr[i]) {
                Integer pop = stack.pop();
                // sum[i - 1] - sum[stack.peek()] 表⽰找到左边第⼀个⽐a[pop]⼤的
                int value = stack.isEmpty() ? sum[i - 1] : (sum[i - 1] - sum[stack.peek()]);
                max = Math.max(max, value * arr[pop]);
            }
            // 栈⾥要从底到顶依次增⼤ , 若进来⼀个⽐顶部⼩的, 要挨个弹出,直到进来这个⽐顶部⼤
            stack.push(i);
        }
        while (!stack.isEmpty()) {
            Integer pop = stack.pop();
            int value = stack.isEmpty() ? sum[sum.length - 1] : (sum[sum.length - 1] - sum[stack.peek()]);
            max = Math.max(max, value * arr[pop]);
        }
        return max;
    }
}

树形DP和Morris遍历

树形动态规划**(DP)**

前提:如果题⽬求解⽬标是S规则,则求解流程可以定成:

以每⼀个节点为头节点的⼦树在S规则下的每⼀个答案,并且最终答案⼀定在其中

树形dp套路:

  1. 以某个节点X为头节点的⼦树中,分析答案有哪些可能性,

    1. 并且这种分析是以X的左⼦树、X的右⼦树和X整棵树的⾓度来考虑可能性的
  2. 根据第⼀步的可能性分析,列出所有需要的信息

  3. 合并第⼆步的信息,对左树和右树提出同样的要求,并写出信息结构

  4. 设计递归函数,递归函数是处理以X为头节点的情况下的答案。

    1. 包括设计递归的base case,默认直接得到左树和右树的所有信息,以及把可能性做整合,并且要返回第三步的信息结

构这四个⼩步骤

难点:情况分类

⼆叉树结点间的最⼤距离问题

题⽬:从⼆叉树的节点a出发,可以向上或者向下⾛,但沿途的节点只能经过⼀次,到达节点b时路径上的节点个数叫作a到b的距离

那么⼆叉树任何两个节点之间都有距离,求整棵树上的最⼤距离。

分析:最⼤距离有两种情况:

  1. 头节点不参与。那最⼤距离 = max(左树最⼤距离,右树最⼤距离)

  2. 头节点参与。那最⼤距离 = 左树⾼ + 右树⾼ + 1

故最⼤距离 = max(左树最⼤距离,右树最⼤距离,左树⾼ + 右树⾼ + 1)

public class MaxDistance {

    class Node {
        int value;
        Node left;
        Node right;

        Node(int val) {
            value = val;
        }
    }

    class Info {
        public int maxDistance;
        public int height;

        public Info(int maxDistance, int height) {
            this.maxDistance = maxDistance;
            this.height = height;
        }
    }

    public int maxDistance(Node head) {
        return process(head).maxDistance;
    }

    private Info process(Node head) {
        if (head == null) {
            return new Info(0, 0);
        }
        Info left = process(head.left);
        Info right = process(head.right);
        // 头节点参与
        int includeHeadDistance = left.height + right.height + 1;
        // 三者中最⼤的就是head结点的最⼤距离
        int maxDistance = Math.max(Math.max(left.maxDistance, right.maxDistance), includeHeadDistance);
        return new Info(maxDistance, Math.max(left.height, right.height) + 1);
    }
}

派对的最⼤快乐值

员⼯信息的定义如下:

class Employee {
     // 这名员⼯可以带来的快乐值
    public int happy; 
    // 这名员⼯有哪些直接下级 
    List<Employee> subordinates; 
}

公司的每个员⼯都符合 Employee 类的描述。

  • 整个公司的⼈员结构可以看作是⼀棵标准的、 没有环的多叉树。

  • 树的头节点是公司唯⼀的⽼板。

  • 除⽼板之外的每个员⼯都有唯⼀的直接上级。

  • 叶节点是没有任何下属的基层员⼯(subordinates列表为空),除基层员⼯外,每个员⼯都有⼀个或多个直接下级。

这个公司现在要办party,你可以决定哪些员⼯来,哪些员⼯不来。但是要遵循如下规则:

  1. 如果某个员⼯来了,那么这个员⼯的所有直接下级都不能来

  2. 派对的整体快乐值是所有到场员⼯快乐值的累加

  3. 你的⽬标是让派对的整体快乐值尽量⼤

给定⼀棵多叉树的头节点boss,请返回派对的最⼤快乐值。

分析:头节点 x 参与还是不参与

x 参与:X.happy + sum(x某个下级在⾃⼰不来时的整体快乐值)

x 不参与:sum(max(x某个下级⾃⼰来时的整体快乐值,x某个下级在⾃⼰不来时的整体快乐值))

public class MaxHappy {

    @Test
    public void test() {
        ArrayList<Employee> list = new ArrayList<>();
        list.add(new Employee(100,new ArrayList<>()));
        list.add(new Employee(90,new ArrayList<>()));
        Employee boss = new Employee(330,list);
        System.out.println(maxHappy(boss));
    }

    class Employee {
        // 这名员⼯可以带来的快乐值
        public int happy;
        // 这名员⼯有哪些直接下级
        List<Employee> subordinates;

        public Employee(int happy, List<Employee> subordinates) {
            this.happy = happy;
            this.subordinates = subordinates;
        }
    }

    class Info {
        public int presentHappy;
        public int absentHappy;

        public Info(int presentHappy, int absentHappy) {
            this.presentHappy = presentHappy;
            this.absentHappy = absentHappy;
        }
    }

    public int maxHappy(Employee x) {
        Info info = process(x);
        return Math.max(info.presentHappy, info.absentHappy);
    }

    private Info process(Employee x) {
        if (x.subordinates.isEmpty()) {
            // base case : x是基层员⼯
            return new Info(x.happy, 0);
        }
        // x来得到的收益
        int present = x.happy;
        // x不来得到的收益
        int absent = 0;
        for (Employee subordinate : x.subordinates) {
            Info nextInfo = process(subordinate);
            // x来得到的收益 + 直接员⼯不来的带的收益
            present += nextInfo.absentHappy;
            // x不来 + 直接员⼯来或不来的最⼤收益
            absent += Math.max(nextInfo.presentHappy, nextInfo.absentHappy);
        }
        return new Info(present,absent);
    }
}

Morris遍历

Morris遍历,是⼀种遍历⼆叉树的⽅式,并且时间复杂度O(N),额外空间复杂度O(1)

通过利⽤原树中⼤量空闲指针的⽅式,达到节省空间的⽬的

笔试⾥⾯不要⽤

遍历细节

假设来到当前节点cur,开始时cur来到头节点位置

  1. 如果 cur 没有左孩⼦,cur 向右移动 (cur = cur.right)

  2. 如果 cur 有左孩⼦,找到左⼦树上最右的节点 mostRight:

    1. 如果 mostRight 的右指针指向空,让其指向 cur, 然后 cur 向左移动 (cur = cur.left)

    2. 如果 mostRight 的右指针指向 cur,让其指向 null, 然后cur向右移动 (cur = cur.right)

  3. cur 为空时遍历停⽌

特点:

有左树的结点可以到达两次,没有左树的结点可以到达1次

第⼀次到达的时候,左树的最右结点指向空

第⼆次到达的时候,左树的最右结点指向⾃⼰

遍历顺序:1,2,4,2,5,1,3,6,3,7

每个结点遍历左⼦树右边界的时候,路过的点都是不重复的,时间复杂度还是O(N)

public class Morris {
    class Node {
        int value;
        Node left;
        Node right;

        Node(int val) {
            value = val;
        }
    }

    public void process(Node head) {
        if (head == null) {
            return;
        }
        Node cur = head;
        Node mostRight = null;
        while (cur != null) {
            // mostRight访问cur的左孩⼦
            mostRight = cur.left;
            // 说明cur有左孩⼦
            if (mostRight != null) {
                while (mostRight.right != null && mostRight.right != cur) {
                    // mostRight向右⾛⼀步
                    mostRight = mostRight.right;
                }
                // mostRight来到了cur左⼦树的最右处
                // 第⼀次来到cur
                if (mostRight.right == null) {
                    mostRight.right = cur;
                    cur = cur.left;
                    continue; 
                }else {
                    // mostRight右孩⼦是cur , 第⼆次来到cur
                    mostRight.right = null;
                }
            }
            cur = cur.right;
        }
    }
}

先序遍历

如果⼀个结点,只到它达⼀次,直接打印

如果可以到达它两次,第⼀次到达时打印

public void morrisPre(Node head) {
    if (head == null) {
        return;
    }
    Node cur = head;
    Node mostRight = null;
    while (cur != null) {
        mostRight = cur.left;
        if (mostRight != null) {
            while (mostRight.right != null && mostRight.right != cur) {
                mostRight = mostRight.right;
            }
            // cur第⼀次来到这个结点
            if (mostRight.right == null) {
                mostRight.right = cur;
                // 这个点有左⼦树,可以到达两次,这是第⼀次到达,所以直接打印
                System.out.println(cur.value + "");
                cur = cur.left;
                continue;
            } else {
                mostRight.right = null;
            }
        } else {
            // 这个点没有左⼦树,所以只能到达⼀次,直接打印
            System.out.println(cur.value + "");
        }
        cur = cur.right;
    }
}

中序遍历

如果⼀个结点,只到它达⼀次,直接打印

如果可以到达它两次,第⼆次到达时打印

public void morrisIn(Node head) {
    if (head == null) {
        return;
    }
    Node cur = head;
    Node mostRight = null;
    while (cur != null) {
        mostRight = cur.left;
        if (mostRight != null) {
            while (mostRight.right != null && mostRight.right != cur) {
                mostRight = mostRight.right;
            }
            // cur第⼀次来到这个结点
            if (mostRight.right == null) {
                mostRight.right = cur;
                cur = cur.left;
                continue;
            } else {
                mostRight.right = null;
            }
        }
        System.out.println(cur.value + "");
        cur = cur.right;
    }
}

后续遍历

较难

如果⼀个结点,只到它达⼀次,不操作

如果可以到达它两次,第⼆次到达时,逆序打印这个结点左树的右边界最后再逆序打印整树的右边界

public void morrisPost(Node head) {
    if (head == null) {
        return;
    }
    Node cur = head;
    Node mostRight = null;
    while (cur != null) {
        mostRight = cur.left;
        if (mostRight != null) {
            while (mostRight.right != null && mostRight.right != cur) {
                mostRight = mostRight.right;
            }
            // cur第⼀次来到这个结点
            if (mostRight.right == null) {
                mostRight.right = cur;
                cur = cur.left;
                continue;
            } else {// 第⼆次到达这个点
                mostRight.right = null;
                // 逆序打印这个结点左树的右边界
                printEdge(cur.left);
            }
        }
        cur = cur.right;
    }
    // 逆序打印整树的右边界
    printEdge(head);
}

/**
     * 逆序打印
     * @param head
     */
private void printEdge(Node head) {
    Node tail = reverseEdge(head);
    Node cur = tail;
    while (cur != null) {
        System.out.println(cur.value + "");
        cur = cur.right;
    }
    reverseEdge(tail);
}

/**
     * 翻转链表
     * @param head
     * @return
     */
private Node reverseEdge(Node head) {

    Node cur = head;
    Node pre = null;
    Node next = null;
    while (cur != null) {
        next = cur.right;
        cur.right = pre;
        pre = cur;
        cur = next;
    }
    return pre;
}

判断搜索⼆叉树

搜索⼆叉树中序遍历是递增的

public boolean isBST5(Node head) {
    if (head == null) {
        return true;
    }
    Node cur = head;
    Node mostRight = null;
    int preValue = Integer.MIN_VALUE;
    while (cur != null) {
        // mostRight访问cur的左孩⼦
        mostRight = cur.left;
        // 说明cur有左孩⼦
        if (mostRight != null) {
            while (mostRight.right != null && mostRight.right != cur) {
                // mostRight向右⾛⼀步
                mostRight = mostRight.right;
            }
            // mostRight来到了cur左⼦树的最右处
            // 第⼀次来到cur
            if (mostRight.right == null) {
                mostRight.right = cur;
                cur = cur.left;
                continue;
            } else {
                // mostRight右孩⼦是cur , 第⼆次来到cur
                mostRight.right = null;
            }
        }
        // 要么是跳过了if,说明没有左树,直接打印
        // 要么是从else⾥刚出来,说明第⼆次来到这个结点,打印
        if (cur.value <= preValue) {
            return false;
        }
        // 说明到这还是搜索⼆叉树,更新preValue
        preValue = cur.value;
        cur = cur.right;
    }
    return true;
}

这两种⽅法的选择

第三次⽅法的强整合:⽤树形DP套路

否则使⽤Morris遍历

⼤数据

⼤数据问题 或者 资源限制问题

基本技巧

  1. 哈希函数:哈希函数可以把数据按照种类均匀分流

  2. 布隆过滤器:布隆过滤器⽤于集合的建⽴与查询,并可以节省⼤量空间

  3. ⼀致性哈希:⼀致性哈希解决数据服务器的负载管理问题

  4. 并查集:利⽤并查集结构做岛问题的并⾏计算

  5. 位图:位图解决某⼀范围上数字的出现情况,并可以节省⼤量空间

  6. 分段统计:利⽤分段统计思想、并进⼀步节省⼤量空间

  7. 堆和外排序:利⽤堆、外排序来做多个处理单元的结果合并

题⽬

没出现过的数

分段统计思想

题⽬:32位⽆符号整数的范围是0~4,294,967,295(2^32 -1) ,现在有⼀个正好包含40亿个⽆符号整数的⽂件,所以在整个范围中必然存在没出现过的数。

  1. 可以使⽤最多1GB的内存,怎么找到所有未出现过的数?

  2. 内存限制为 10kB,但是只⽤找到⼀个没出现过的数即可

对于题1:

取值范围和位图做对应,占⽤:2^32 /8 = 500M,

哪个数字有了就涂⿊,遍历结束且没有被涂⿊的位置就是没出现过的数

对于题2:

一个int是4B,将内存限制 10kB/4B = 2500, 得到⼀个数字2500,就是内存最多支持开辟2500个int,将这个数字向下取到最⼤的 2 的次⽅数 n = 2048 = 2^11,向下取是为了保证不超过内存

然后将 0~2^32 平均分成 2048 份,每份个数为 a = 2^32 / n

类似于桶排序思想,遍历⼀遍,看看哪个桶没满,说明区间有⼀个数没出现过

具体的:每个数 /a 得到 i,就把数组arr[i]++ ,因为只有40亿个数,肯定存在某个范围不够a个,里面肯定存在没出现过的数字,把这个范围在平均分成 2048 份,重复上面的步骤肯定可以得到答案

再极端⼀点:

只能申请10个以内的变量 – 在2^32范围上⼆分,总有⼀边不满,在不满的一边继续二分,最多过 32 次文件

找出重复的URL

题⽬:有⼀个包含100亿个URL的⼤⽂件,假设每个URL占⽤64B,请找出其中所有重复的URL

可以⽤布隆过滤器、哈希函数分流

补充:某搜索公司一天的用户搜索词汇是海量的(百亿数据量),请设计一种求出每天热门Top100
词汇的可行办法

  1. 哈希函数分流

  2. 建⽴⼆维堆

    1. 每个⽂件⼀个大顶堆统计词频

    2. 每个堆的堆顶拿出来组成总堆

    3. 返回总堆堆顶元素,找到该元素来自哪个堆,再拿出来堆顶组成总堆

⽤堆来实现多个单元的结果合并

出现两次的数和中位数

题⽬:现在有⼀个正好包含40亿个⽆符号整数的⽂件

  1. 可以使⽤最多1GB的内存,怎么找到所有出现了两次的数?

  2. 内存限制为 10kB,找到中位数

对于题⽬1:

可以使⽤哈希函数分流

可以⽤位图中每两位表⽰⼀个数字出现的次数 00,01,10,11

需要空间 a = 2^32 * 2 / 8B, 1GB内存是足够的

对于题⽬2:

⽤分段统计的思想:

个数2^32

将内存限制 10kB/4 = 2500, 得到⼀个数字2500,将这个数字向下取到最⼤的 2 的次⽅数 n = 2048 = 2

然后将 0~2^32 平均分成 2048 份,每份个数为 a = 2^32 / n

遍历所有数字,每个数字/2048,如17/2048 = 0,那arr[0]++

最后可以知道每个区间有多少个,中位数即为第20亿个数和第20亿+1个数的平均值

做累加和,第⼀个超过20亿的区间就是⽬标区间,即定位到⼩范围,逐步缩⼩范围

⼤数据排序

题⽬:10G的⽂件,有符号整数( -2^31 ~ 2^31 - 1 ),⽆序的,只给5G/5k内存,输出⼀个新的⽂件,使其有序

⽅法⼀:分段统计 + 堆

  • ⼩根堆中⼀个数据要占16字节,5G是5 * 2^30 ,也就是⼩根堆⾥能放 5 * 2^26 个,最近的是 2^27
  • 将 -2^31 ~ 2^31 -1 分成 2^5 份(区间)
  • 遍历⽂件,⼀个数如果在第⼀个区间,放到内存⾥组成⼩根堆,并记录它出现次数,完成后放到新⽂件⾥
  • 再遍历⽂件,对下个区间继续⽤⼩根堆排序后依次弹出到新⽂件末尾

⽅法⼆:分段统计 + 堆

  • 建⽴⼀个只能放少量数字的⼤根堆(如容量n=100万)

  • 遍历⽂件,将最⼩的100万个数字放⼊⼤根堆,具体就是:

    • 不到100万个直接往⾥放

    • 否则,数字在⾥⾯,就记录数⽬++;

    • 数字不在⾥⾯,若⼤于堆顶,不加⼊

    • 数字不在⾥⾯,若⼩于堆顶,堆顶弹出,该数加⼊

  • 遍历完成后,在新⽂件⾥从⼩到⼤打印⼤根堆⾥的数⽬,⽤⼀个全局变量 Y 记录⼤根堆顶的数字

  • 重新遍历⽂件,将⼤于 Y 的最⼩的100万个数字放⼊⼤根堆,重复上述操作,具体就是

    • ⼩于 Y 的数字直接跳过

    • ⼤于 Y 的数字和上⾯⼀样

  • 直到完全遍历完成

位运算

基本技巧

提取符号:n >> 31

相反数:~n+1

取到最右端的1: X & (~X + 1)

是不是只有⼀个1:X & (X-1) == 0

⽤16进制的⼀个数,表⽰2进制的4位:0x55 = 0101 0101

⼆进制的加、减、乘、除

题⽬

返回两数较⼤的

题⽬:给定两个有符号32位整数a和b,返回a和b中较⼤的。

要求:不⽤做任何⽐较判断

public class MaxNum {

    @Test
    public void test() {
        System.out.println(getMax1(2, 6));
    }

    /**
     * 有可能溢出
     *
     * @param a
     * @param b
     * @return
     */
    public int getMax1(int a, int b) {
        int c = a - b;
        // 提取差的符号位,并返回正负号
        int scA = sign(c);
        // 反转scB
        int scB = flip(scA);
        return scA * a + scB * b;
    }

    /**
     * 考虑溢出
     *
     * @param a
     * @param b
     * @return
     */
    public int getMax2(int a, int b) {
        int c = a - b;
        int sa = sign(a);
        int sb = sign(b);
        int sc = sign(c);
        int difSab = sa ^ sb;
        int sameSab = flip(difSab);
        // ab符号不同,且a正 或者 ab符号相同, a-b为正 ==> 返回a,否则返回b
        int returnA = difSab * sa + sameSab * sc;
        int returnB = flip(returnA);
        return a * returnA + b * returnB;
    }

    /**
     * 反转正负号
     * @param n
     * @return
     */
    private int flip(int n) {
        return n ^ 1;
    }

    /**
     * 判断它是不是正数
     *
     * @param n
     * @return
     */
    private int sign(int n) {
        return flip((n >> 31) & 1);
    }
}
判断幂

题⽬:判断⼀个32位正数是不是2的幂、4的幂

分析:

1:0000 0001

2:0000 0010

4:0000 0100

8:0000 1000

如果⼀个数是2的幂,这个数的⼆进制只能有⼀个1

对于2的幂

⽅法⼀:

取到最右端的1:令Y = X & (~X + 1)

若Y == X,则说明只有⼀个1,是2的幂,否则有多个1,不是2的幂

⽅法⼆:

2^n -1 会把最后⼀位的1打散,如0000 1000 -1 = 0000 0111

故,若:X & (X-1) == 0,那说明只有⼀个1,是2的幂,否则有多个1,不是2的幂

对于4的幂

如果⼀个数是4的n次幂,这个数的⼆进制只能有⼀个1,且后⾯有2n个0

思路:

先判断是不是 2 的幂,如果不是 2的幂,那⼀定不是4的幂;如果是2的幂继续下⼀步

令Y = 0x55555555 = 0101 … 0101(32位),如果 X & Y != 0,那就是4的幂,否则不是

public class Power {
    @Test
    public void test() {
        System.out.println(is4Power(16));
    }

    public boolean is2Power1(int n) {
        return (n & (n - 1)) == 0;
    }

    public boolean is2Power2(int n) {

        return (n & (~n + 1)) == n;
    }

    public boolean is4Power(int n) {
        return is2Power1(n) && (n & 0x55555555) != 0;
    }
}
加减乘除

题⽬:给定两个有符号32位整数a和b,不能使⽤算术运算符,分别实现a和b的加、减、乘、除运算

  1. 加法:

    异或:a^b 为a、b⽆进位相加结果

    与:a&b 为a、b的进位信息

    a+b = a^b + a&b<<1

    上⼀步使⽤了加法,继续使⽤异或和与,直到进位信息为0,那该步的⽆进位相加结果为最终结果

    如:7+13 = 20

7 = 00111

13 = 01101

7^13 = 01010

7&13<<1 = 01010

再异或 = 00000

与左移 = 10100

再异或 = 10100 = 结果 = 20

与左移 = 00000 = 0

  1. 减法

    a - b = a + (b的相反数)

    b的相反数 = b取反 + 1

  2. 乘法

    乘法竖式

13 = 01101

6 = 00110


00000 +

01101 + (13 << 1)

01101 + (13 << 1)


乘法转化成加法

b的末尾为0

​ a左移末尾补0

​ b⽆符号右移

b的末尾为1

​ 累加a

​ a左移末尾补0

​ b⽆符号右移

当b==0时结束

  1. 除法

    a / b

    b不断左移,当b第⼀次⼤于a时,a = a-b,假如b移动了3位,那得到的第⼀个结果为1000

    b左移2位,a再尝试减去b,若可以,那得到的第⼆个数为0100

    最后不能再减,结果为⼏个数相加,1100

    其实就是乘法的逆过程

public class ASMD {

    @Test
    public void test() {
        System.out.println(divide(9, 6) );
    }

    /**
     * a+b溢出不予以考虑
     */
    public int add(int a, int b) {

        int sum = a;
        // 进位信息为0,结束
        while (b != 0) {
            // 不进位和
            sum = a ^ b;
            // 进位信息
            b = (a & b) << 1;
            a = sum;
        }
        return sum;
    }

    public int minus(int a, int b) {
        return add(a, negNum(b));
    }

    public int multi(int a, int b) {
        int res = 0;
        while (b != 0) {
            //b末尾为1
            if ((b & 1) != 0) {
                res = add(res, a);
            }
            a <<= 1;
            b >>>= 1;
        }
        return res;
    }


    public int divide(int a, int b) {
        int x = isNeg(a) ? negNum(a) : a;
        int y = isNeg(b) ? negNum(b) : b;
        int res = 0;
        for (int i = 31; i >= 0; i = minus(i, 1)) {
            // 让x左移,不让y右移(有可能溢出)
            if ((x >> i) >= y) {
                res |= (1 << i);
                x = minus(x, y << i);
            }
        }
        return isNeg(a) ^ isNeg(b) ? negNum(res) : res;
    }

    private boolean isNeg(int n) {
        return n < 0;
    }

    private int negNum(int n) {
        // n取反 + 1
        return add(~n, 1);
    }
}

从暴⼒递归到动态规划

基本思路

如何从⼀种暴⼒递归的⽅法优化到动态规划

  • 尝试⽅法

  • 对尝试⽅法进⾏优化

    • 记忆搜索
      • 位置依赖
  • 严格表结构

  • 严格表(精)

⼀个尝试⼀旦确定,判断变量个数和范围,添加缓存空间,即为记忆化搜索

在此基础上,再加上最终的位置,根据basecase推出直接直到的位置,根据递归关系推导依赖关系,确定计算顺序,即为严格表结构

题⽬

机器⼈运动

题⽬:机器⼈初始位置 S = 1~N ,结尾位置 P = 1~N

机器⼈必须⾛ k 步,从S到P ,机器⼈每步必须⾛,往左⾛或往右⾛,⽅法数为多少

先想怎么试,再优化

版本1:暴⼒递归,时间复杂度:O(2 )

public int ways1(int N, int S, int K, int P) {

    return walk1(N, P, S, K);
}

/**
     * 暴力递归
     */
private int walk1(int N, int P, int cur, int rest) {
    if (rest == 0) {
        return cur == P ? 1 : 0;
    }
    if (cur == 1) {
        return walk1(N, P, cur + 1, rest - 1);
    }
    if (cur == N) {
        return walk1(N, P, cur - 1, rest - 1);
    }
    return walk1(N, P, cur + 1, rest - 1) + walk1(N, P, cur - 1, rest - 1);
}

版本2:记忆化搜索

时间复杂度:O(K*N)

public int ways2(int N, int S, int K, int P) {
    int[][] dp = new int[S + 1][K + 1];
    for (int i = 0; i <= S; i++) {
        Arrays.fill(dp[i], -1);
    }
    return walk2(N, P, S, K, dp);
}

/**
     * 记忆化搜索
     */
private int walk2(int N, int P, int cur, int rest, int[][] dp) {
    //缓存中有就直接拿,避免重复计算
    if (dp[cur][rest] != -1) {
        return dp[cur][rest];
    }
    if (rest == 0) {
        dp[cur][rest] = cur == P ? 1 : 0;
        return dp[cur][rest];
    }
    if (cur == 1) {
        dp[cur][rest] = walk1(N, P, cur + 1, rest - 1);
        return walk1(N, P, cur + 1, rest - 1);
    } else if (cur == N) {
        dp[cur][rest] = walk1(N, P, cur - 1, rest - 1);
    }else{
        dp[cur][rest] = walk1(N, P, cur + 1, rest - 1) + walk1(N, P, cur - 1, rest - 1);
    }
    return  dp[cur][rest];
}

版本3:严格表结构的DP

根据记忆化搜索中表内的位置依赖(递归关系),来依次填表

时间复杂度:O(K*N)

/**
     * 严格表结构
     */
public int ways3(int N, int P, int cur, int rest) {
    int[][] dp = new int[rest + 1][N + 1];
    dp[0][P] = 1;
    for (int i = 1; i <= rest; i++) {
        for (int j = 1; j <= N; j++) {
            if (j == 1) {
                dp[i][j] = dp[i - 1][j + 1];
            } else if (j == N) {
                dp[i][j] = dp[i - 1][j - 1];
            } else {
                dp[i][j] = dp[i - 1][j - 1] + dp[i - 1][j + 1];
            }
        }
    }
    return dp[rest][cur];
}
最少货币数

题⽬:给定数组 arr,arr 中所有的值都为正数且不重复。

每个值代表⼀种⾯值的货币,每种⾯值的货币可以使⽤1张

再给定⼀个整数 aim,代表要找的钱数,求组成 aim 的最少货币数。

从左往右的尝试模型

版本1:暴⼒递归

public int minCoins1(int[] arr, int aim) {
    return process1(arr, aim, 0);
}

private int process1(int[] arr, int rest, int index) {
    if (rest < 0) {
        return -1;
    }
    if (rest == 0) {
        return 0;
    }
    if (index == arr.length) {
        return -1;
    }

    int p1 = process1(arr, rest, index + 1);
    int p2 = process1(arr, rest - arr[index], index + 1);
    if (p1 == -1 && p2 == -1) {
        return -1;
    }else{
        if (p1 == -1) {
            return p2 + 1;
        }
        if (p2 == -1) {
            return p1;
        }
        return Math.min(p1, 1 + p2);
    }
}

版本2:记忆化搜索

public int minCoins2(int[] arr, int aim) {
    int[][] dp = new int[arr.length + 1][aim + 1];
    for (int i = 0; i <= arr.length; i++) {
        Arrays.fill(dp[i], -2);
    }
    return process2(arr, aim, 0, dp);
}

private int process2(int[] arr, int rest, int index, int[][] dp) {
    if (rest < 0) {
        return -1;
    }
    if (dp[index][rest] != -2) {
        return dp[index][rest];
    }
    if (rest == 0) {
        dp[index][rest] = 0;
    } else if (index == arr.length) {
        dp[index][rest] = -1;
    } else {
        int p1 = process1(arr, rest, index + 1);
        int p2 = process1(arr, rest - arr[index], index + 1);
        if (p1 == -1 && p2 == -1) {
            dp[index][rest] = -1;
        } else {
            if (p1 == -1) {
                dp[index][rest] = p2 + 1;
            }
            if (p2 == -1) {
                dp[index][rest] = p1;
            }
            dp[index][rest] = Math.min(p1, 1 + p2);
        }
    }
    return dp[index][rest];
}

版本3:严格表结构的DP

public int minCoins3(int[] arr, int aim) {
    int N = arr.length;
    int[][] dp = new int[N + 1][aim + 1];
    for (int i = 0; i <= N; i++) {
        dp[i][0] = 0;
    }
    for (int i = 1; i <= aim; i++) {
        dp[N][i] = -1;
    }
    // 改写递归
    for (int index = N - 1; index >= 0; index--) {
        for (int rest = 1; rest <= aim; rest++) {
            int p1 = dp[index + 1][rest];
            int p2 = -1;
            if (rest - arr[index] >= 0) {
                p2 = dp[index + 1][rest - arr[index]];
            }
            if (p1 == -1 && p2 == -1) {
                dp[index][rest] = -1;
            } else {
                if (p1 == -1) {
                    dp[index][rest] = p2 + 1;
                } else if (p2 == -1) {
                    dp[index][rest] = p1;
                } else {
                    dp[index][rest] = Math.min(p1, 1 + p2);
                }
            }
        }
    }
    return dp[0][aim];
}
拿牌游戏

给定⼀个整型数组arr,代表数值不同的纸牌排成⼀条线。

玩家A和玩家B依次拿⾛每张纸牌,规定玩家A先拿,玩家B后拿,但是每个玩家每次只能拿⾛最左或最右的纸牌,玩家A和玩家B都绝顶聪 明。

请返回最后获胜者的分数。 0

版本1:暴⼒递归

public class CardsInLine {

    @Test
    public void test() {
        System.out.println(win1(new int[]{1, 2, 100, 4}));
    }

    public int win1(int[] arr) {
        if (arr == null || arr.length == 0) {
            return 0;
        }
        return Math.max(f(arr, 0, arr.length - 1), s(arr, 0, arr.length - 1));
    }

    private int f(int[] arr, int l, int r) {
        if (l == r) {
            return arr[l];
        }
        return Math.max(arr[l] + s(arr, l + 1, r), arr[r] + s(arr, l, r - 1));
    }

    private int s(int[] arr, int l, int r) {
        if (l == r) {
            return 0;
        }
        return Math.min(f(arr, l + 1, r), f(arr, l, r - 1));
    }
}

版本2:严格表结构的DP

public class CardsInLine {

    @Test
    public void test() {
        System.out.println(win1(new int[]{1, 2, 100, 4}));
    }
	public int win2(int[] arr) {
        if (arr == null || arr.length == 0) {
            return 0;
        }
        int[][] f = new int[arr.length][arr.length];
        int[][] s = new int[arr.length][arr.length];
        for (int i = 0; i < arr.length; i++) {
            f[i][i] = arr[i];
        }
        int row = 0;
        int col = 1;
        while (col < arr.length) {

            int i = row;
            int j = col;
            while (i < arr.length && j < arr.length) {
                f[i][j] = Math.max(arr[i] + s[i + 1][j], arr[j] + s[i][j - 1]);
                s[i][j] = Math.min(f[i + 1][j], f[i][j - 1]);
                i++;
                j++;
            }
            col++;
        }
        return Math.max(f[0][arr.length - 1], s[0][arr.length - 1]);
    }
}
象棋中马的跳法

题⽬:把整个棋盘放⼊第⼀象限,棋盘的最左下⾓是(0,0)位置。

那么整个棋盘就是横坐标上9条线、纵坐标上10条线的⼀个区域。

给三个参数,x,y,k,返回如果“马”从(0,0)位置出发,必须⾛k步,最后落在(x,y)上的⽅法数有多少种?

版本1:暴⼒递归

public class ChessHorse {

    @Test
    public void test() {
        System.out.println(getWays(4, 5, 6));
    }

    public int getWays(int x, int y, int k) {
        return process(x, y, k);
    }

    private int process(int x, int y, int step) {
        if (x < 0 || x > 8 || y < 0 || y > 9) {
            return 0;
        }
        if (step == 0) {
            return (x == 0 && y == 0) ? 1 : 0;
        }
        return process(x - 1, y + 2, step - 1) +
                process(x + 1, y + 2, step - 1) +
                process(x + 2, y + 1, step - 1) +
                process(x + 2, y - 1, step - 1) +
                process(x + 1, y - 2, step - 1) +
                process(x - 1, y - 2, step - 1) +
                process(x - 2, y - 1, step - 1) +
                process(x - 2, y + 1, step - 1);
    }
}

版本2:严格表结构的DP

三维表结构

public class ChessHorse {

    @Test
    public void test() {
        System.out.println(getWays(4, 5, 6));
    }
	public int getWays2(int x, int y, int step) {
        if (x < 0 || x > 8 || y < 0 || y > 9) {
            return 0;
        }
        int[][][] dp = new int[9][10][step + 1];
        dp[0][0][0] = 1;
        for (int h = 1; h <= step; h++) {
            for (int r = 0; r < 9; r++) {
                for (int c = 0; c < 10; c++) {
                    dp[r][c][h] += getValue(dp, r - 1, c + 2, h - 1);
                    dp[r][c][h] += getValue(dp, r + 1, c + 2, h - 1);
                    dp[r][c][h] += getValue(dp, r + 2, c + 1, h - 1);
                    dp[r][c][h] += getValue(dp, r + 2, c - 1, h - 1);
                    dp[r][c][h] += getValue(dp, r + 1, c - 2, h - 1);
                    dp[r][c][h] += getValue(dp, r - 1, c - 2, h - 1);
                    dp[r][c][h] += getValue(dp, r - 2, c - 1, h - 1);
                    dp[r][c][h] += getValue(dp, r - 2, c + 1, h - 1);
                }
            }
        }
        return dp[x][y][step];
    }

    public static int getValue(int[][][] dp, int row, int col, int step) {
        if (row < 0 || row > 8 || col < 0 || col > 9) {
            return 0;
        }
        return dp[row][col][step];
    }
}

Bob的⽣存概率

题⽬:给定五个参数n,m,i,j,k。

表⽰在⼀个N*M的区域,Bob处在(i,j)点,每次Bob等概率的向上、下、左、右四个⽅向移动⼀步,Bob必须⾛K步。

如果⾛完之后,Bob还停留在这个区域上,并且期间没有出过格⼦,就算Bob存活,否则就算Bob死亡。

请求解Bob的⽣存概率,返回字符串表⽰分数的⽅式。

版本1:暴⼒递归

public class Survival {

    @Test
    public void test() {
        System.out.println(bob1(5, 6, 1, 3, 5));
    }

    public String bob1(int N, int M, int i, int j, int K) {
        long all = (long) Math.pow(4, K);
        long live = process(N, M, i, j, K);
        // 最⼤公约数
        long gcd = gcd(all, live);
        return (live / gcd) + "/" + (all / gcd);
    }

    private long gcd(long m, long n) {
        return n == 0 ? m : gcd(n, m % n);
    }

    private int process(int n, int m, int i, int j, int k) {
        if (i < 0 || i == n || j < 0 || j == m) {
            return 0;
        }
        if (k == 0) {
            return 1;
        }
        return process(n, m, i + 1, j, k - 1) +
                process(n, m, i, j + 1, k - 1) +
                process(n, m, i - 1, j, k - 1) +
                process(n, m, i, j - 1, k - 1);
    }
}

版本2:题:严格表结构DP

public class Survival {

    @Test
    public void test() {
        System.out.println(bob1(5, 6, 1, 3, 5));
    }
    public String bob2(int N, int M, int i, int j, int K) {
        long all = (long) Math.pow(4, K);
        int[][][] dp = new int[N + 1][M + 1][K + 1];
        for (int row = 0; row < N; row++) {
            for (int col = 0; col < M; col++) {
                dp[row][col][0] = 1;
            }
        }

        for (int h = 1; h <= K; h++) {
            for (int row = 0; row < N; row++) {
                for (int col = 0; col < M; col++) {
                    int up = getValue(dp, N, M, row + 1, col, h - 1);
                    int down =  getValue(dp, N, M, row, col + 1, h - 1);
                    int left = getValue(dp, N, M, row - 1, col, h - 1);
                    int right = getValue(dp, N, M, row, col - 1, h - 1);
                    dp[row][col][h] = up + down + left + right;
                }
            }
        }
        long live = dp[i][j][K];
        // 最⼤公约数
        long gcd = gcd(all, live);
        return (live / gcd) + "/" + (all / gcd);
    }

    public int getValue(int[][][] dp, int N, int M, int row, int col, int K) {
        if (row < 0 || row >= N || col < 0 || col >= M) {
            return 0;
        }
        return dp[row][col][K];
    }
	private long gcd(long m, long n) {
        return n == 0 ? m : gcd(n, m % n);
    }
}
货币的⽅法数

题⽬:给定数组 arr,arr 中所有的值都为正数且不重复。

每个值代表⼀种⾯值的货币,每种⾯值的货币可以使⽤任意张

再给定⼀个整数 aim,代表要找的钱数,求组成 aim 的⽅法数。

版本1:暴⼒递归

从左往右尝试模型:

public class CoinsWay {

    @Test
    public void test() {
        System.out.println(coinsWay1(new int[]{3, 5, 10, 1}, 1000));
    }

    public int coinsWay1(int arr[], int aim) {
        return process(arr, 0, aim);
    }

    private int process(int[] arr, int index, int rest) {

        if (index == arr.length) {
            return rest == 0 ? 1 : 0;
        }
        int ways = 0;
        for (int nums = 0; arr[index] * nums <= rest; nums++) {
            ways += process(arr, index + 1, rest - arr[index] * nums);
        }
        return ways;
    }
}

版本2:严格表结构DP

public class CoinsWay {

    @Test
    public void test() {
        System.out.println(coinsWay1(new int[]{3, 5, 10, 1}, 1000));
    } 
	public int coinsWay2(int[] arr, int aim) {
        if (arr == null || arr.length == 0) {
            return 0;
        }
        int N = arr.length;
        int[][] dp = new int[N + 1][aim + 1];
        dp[N][0] = 1;
        for (int index = N - 1; index >= 0; index--) {
            for (int rest = 0; rest <= aim; rest++) {
                dp[index][rest] = 0;
                for (int nums = 0; arr[index] * nums <= rest; nums++) {
                    dp[index][rest] += dp[index + 1][rest - arr[index] * nums];
                }
            }
        }
        return dp[0][aim];
    }
}

枚举⾏为是否有必要?没必要!

枚举的过程中会有重复

继续优化:( 斜率优化 )

如果填表的时候有枚举⾏为,我看看临近的位置能不能替代枚举⾏为

public class CoinsWay {

    @Test
    public void test() {
        System.out.println(coinsWay1(new int[]{3, 5, 10, 1}, 1000));
    }
	 public int coinsWay3(int[] arr, int aim) {
        if (arr == null || arr.length == 0) {
            return 0;
        }
        int N = arr.length;
        int[][] dp = new int[N + 1][aim + 1];
        dp[N][0] = 1;
        for (int index = N - 1; index >= 0; index--) {
            for (int rest = 0; rest <= aim; rest++) {
                dp[index][rest] = dp[index + 1][rest];
                if (rest - arr[index] >= 0) {
                    dp[index][rest] += dp[index][rest - arr[index]];
                }
            }
        }
        return dp[0][aim];
    }

如何尝试?

尝试过程中要注意两个事情:

  • 每个可变参数⾃⼰的维度最好是零维

    • 最好是零维参数–int类型

    • 不要是数组类型,表的⼀个轴会超级长

  • 可变参数的个数越少越好

    • 可变参数的个数决定表结构的维度

有序表

哪些结构可以实现有序表?

  • 平衡搜索⼆叉树(BST)系列

    • 红⿊树(Red Black Tree)

    • AVL树(发明⼈:Adelson-Velskii 以及 Landis)

    • SB树(Size Balance Tree)

  • 跳表(Skip List)

它们都可以O(logN)时间实现,四种结构区别不⼤

搜索⼆叉树

添加:

  • 结点依次放⼊,⼩于头节点则放左⼦树,⼤于头节点则放右⼦树
  • ⼩于某个点 N 则放 N 的左⼦树,⼤于 N 则放 N 的右⼦树
  • 直到叶⼦

删除:

  • 要删除的结点没有左右孩⼦,直接断掉
  • 左右两个孩⼦不双全,直接孩⼦代替它
  • 都全的⽐较⿇烦,⼀个结点 N 去掉时
  • 让其左树最右 A 替代 N
  • 让其右树最左 B 替代 N ,B 的⽗亲的左指针指向 B 的 右孩⼦

查找,⼀个节点 N ,⽬标值 aim

  • 若aim > N,向 N 的右⼦树寻找
  • 若aim < N,向 N 的左⼦树寻找

搜索⼆叉树不具有平衡性,是否为 O(logN) 取决与数据的输⼊和删除

平衡搜索⼆叉树

具有平衡性,任⼀结点的左树和右树⾼度相差都不超过1

通过左旋和右旋操作来保持搜索树的平衡

image-20220919094247787

左旋:头节点 A 倒向左边,头节点右孩⼦ C 成为了头节点,A 成为 C 的左孩⼦,C 的左孩⼦ F 成为 A 的左孩⼦

左旋后:

image-20220919094321305

右旋:头节点 A 倒向右边,头节点左孩⼦ B 成为了头节点,A 成为 B 的右孩⼦,B 的 右孩⼦ E 成为 A 的左孩⼦

右旋后: image-20220919094350266

怎么发现⾃⼰不平衡?

  • 新增⼀个结点从该结点开始,依次查⽗亲结点是否平衡

  • 删除⼀个结点 N 从替代结点开始,依次查⽗亲结点是否平衡

    • 让其左树最右 A 替代 N

    • 让其右树最左 B 替代 N ,B 的⽗亲的左指针指向 B 的 右孩⼦

⼏种不平衡的类型:

  • LL:左树的左孩⼦太长导致的不平衡:右旋

  • RR:右树的右孩⼦太长导致的不平衡:左旋

  • LR:左树的右孩⼦太长导致的不平衡:让左树的右孩⼦成为头部

    • 先让左树头节点左旋,变成了LL型

    • 再让头节点右旋,平衡

  • RL:右树的左孩⼦太长导致的不平衡:让右树的左孩⼦成为头部

    • 先让右树投机点右旋,变成RR型

    • 再让头节点左旋,平衡

    • 调平衡的代价 O(logN)

调平衡的代价 O(logN)

SB树

平衡性:

⼀个结点的每棵⼦树的⼤⼩,不⼩于其兄弟的⼦树⼤⼩

即:每个⼦树的叔叔的⼤⼩,不⼩于其任何侄⼦树的⼤⼩

调整和AVL树差不多

调整:

LL:头节点 T 的左孩⼦ L 的左孩⼦ A,⽐ T 的右孩⼦ R ⼤:m(T)

  1. 右旋,T 左孩⼦ L 变为头节点

  2. 递归,对孩⼦发⽣变化的结点进⾏调整m(T)、m(L)

LR:头节点 T 的左孩⼦ L 的右孩⼦ B,⽐ T 的右孩⼦ R ⼤:m(T)

  1. 以 L 为头左旋

  2. 再以 T 为头左旋

  3. 递归,对孩⼦发⽣变化的结点进⾏调整m(L)、m(T)、m(B)

红⿊树

新增操作标准–5个

删除结点标准–8个

每个点不是红就是⿊

最底层的空结点(这⾥叫叶节点)和头节点必须为⿊

任何两个红节点不能相邻

任何⼀棵⼦树,每条到结束的路上⿊⾊的结点⼀样多

跳表

扔⾊⼦,0.5概率是0,0.5概率是1、

  • 是1就加⼀个向外指的指针,直到是0停⽌仍

默认结点始终和指针数最多的指针数⼀样多

数据个数N,第0层有N个,1层有N/2个,…,m层有N/2m个

代码参考:https://gitee.com/Zesystem/hongheishuheavlshu/tree/master

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值