读书笔记
(最后更新 )

漫画算法

记录书中面试算法题的解法,主要是书中思路。

如何判断链表有环

给定一个链表,判断链表中是否有环。

循环判断

从头节点开始,依次遍历单链表中的每一个节点。没遍历一个新节点,就从头检查新节点之前的所有节点,查看是否相同。若存在相同节点证明链表是有环的。

public class Solution {
    public boolean hasCycle(ListNode head) {
        if (head == null || head.next == null) return false;
        int total = 2;
        ListNode cur = head.next;
        while (cur != null) {
            int start = 1;
            ListNode node = head;
            while (node != cur) {
                node = node.next;
                start++;
            }
            if (start != total) return true;
            total++;
            cur = cur.next;
        }
        return false;
    }
}

该算法遍历一遍的同时,内部也在从头遍历一次,所以时间复杂度为O(n^2)

没有创造新的内存空间,空间复杂度为O(1)

Set集合

创建一个Set集合,遍历链表并将节点加入Set集合,当出现重复的元素,即加入Set集合失败即代表有环出现。

public class Solution {
    public boolean hasCycle(ListNode head) {
        if (head == null || head.next == null) return false;
        Set<ListNode> set = new HashSet<>();
        while (head != null) {
            if (!set.add(head)) return true;
            head = head.next;
        }
        return false;
    }
}

该算法是遍历一遍链表,所以时间复杂度为O(n)

将所有节点加入Set集合存储,空间复杂度也为O(n)

双指针

使用两个指针,一个每次前进两个,一个每次前进一个,若是出现两个指针指向一个节点,代表链表有环。因为快指针提前入环,总会追上慢指针。

public class Solution {
    public boolean hasCycle(ListNode head) {
        if (head == null || head.next == null) return false;
        ListNode fast = head;
        ListNode slow = head;
        while (fast != null && fast.next != null) {
            fast = fast.next.next;
            slow = slow.next;
            if (fast == slow) return true;
        }
        return false;
    }
}

只用循环一次链表,所以时间复杂度为O(n)

不用创建额外的空间,所以空间复杂度为O(1)

求出环的长度

快慢节点重合的时候,在继续前进并记录前进的次数,下次重合,就是快指针(速度是慢指针的两倍)比慢指针多走了一圈,可以理解为前进的次数就是环的长度。

public class Solution {
    public ListNode getCycleLength(ListNode head) {
        if (head == null || head.next == null) return false;
        ListNode fast = head;
        ListNode slow = head;
        while (fast != null && fast.next != null) {
            fast = fast.next.next;
            slow = slow.next;
            if (fast == slow) break;
        }
        int length = 1;
        fast = fast.next.next;
        slow = slow.next;
        while (fast != slow) {
			fast = fast.next.next;
            slow = slow.next;
            length++;
        }
        return length;
    }
}

判断入环点

假设链表头节点到入环点的距离是D,从入环点到两个指针首次相遇点为S1,从两个指针首次相遇点到入环点的距离为S2

设慢指针的移动速度为一个节点,快指针的移动速度为两个节点。

故两个指针首次相遇时,两个指针所走的距离如下

慢指针行走的距离为D + S1

快指针的速度是慢指针的两倍,所以相当于比慢指针多走了N圈,故行走的距离为D + S1 + N * (S1 + S2)

因为快指针的速度是慢指针的两倍,所以快指针的行走距离应该为慢指针的两倍,所以有

2(D + S1) = D + S1 + N * (S1 + S2)

经过整理可以得到

D = (N - 1)(S1 + S2) + S2

也就是说,链表头节点到入环点的的距离相当于从首次相遇点绕环N - 1圈后再回到入环点的距离。

故从头节点出发,与首次相遇点出发,两者均以一个节点的速度,两者相遇点即为入环点。

public class Solution {
    public ListNode detectCycle(ListNode head) {
        if(head == null || head.next == null) return null;
        ListNode fast = head;
        ListNode slow = head;
        while(fast != null && fast.next != null) {
            slow = slow.next;
            fast = fast.next.next;
            if(slow == fast) {
                fast = head;
                while(fast != slow) {
                   fast = fast.next;
                   slow = slow.next;
                }
                return fast;
            }
        }
        return null;
    }
}

最小栈的实现

设计一个支持 pushpoptop 操作,并能在常数时间内检索到最小元素的栈。

  • push(x) —— 将元素 x 推入栈中。
  • pop() —— 删除栈顶的元素。
  • top() —— 获取栈顶元素。
  • getMin() —— 检索栈中的最小元素。

这里需要使用到辅助栈来实现,将每次最小的元素都放到辅助栈中,若即入栈的元素比最小元素小,辅助栈中就放入该最小元素,否则就放入之前最小的元素。当栈出栈后,辅助栈也跟着出栈。

class MinStack {
    Stack<Integer> min;
    Stack<Integer> stack;
    public MinStack() {
        stack = new Stack<>();
        min = new Stack<>();
    }
    
    public void push(int val) {
        if (stack.isEmpty()) {
            stack.push(val);
            min.push(val);
        } else {
            stack.push(val);
            if (val < min.peek()) {
                min.push(val);
            } else {
                min.push(min.peek());
            }
        }
    }
    
    public void pop() {
        if (stack.isEmpty()) {}
        stack.pop();
        min.pop();
    }
    
    public int top() {
        return stack.peek();
    }
    
    public int getMin() {
        return min.peek();
    }
}

/**
 * Your MinStack object will be instantiated and called as such:
 * MinStack obj = new MinStack();
 * obj.push(val);
 * obj.pop();
 * int param_3 = obj.top();
 * int param_4 = obj.getMin();
 */

如何求出最大公约数

求出两个正整数的最大公约数。

枚举

从较小整数的一半开始遍历到2,首次出现了两个正整数都整除的数,则该数就是两个正整数的最大公约数。

public static int getGreatestCommonDivisor(int a, int b) {
        int max = Math.max(a, b);
        int min = Math.min(a, b);
        if (max % min == 0) {
            return min;
        }
        for (int i =  min / 2; i > 1; i--) {
            if (min % i == 0 && max % i == 0) {
                return i;
            }
        }
        return 1;
    }

时间复杂度为O(min(a, b))

辗转相除法(欧几里得算法)

这个方法基于一个定理:

两个正整数a和b(a > b),它们的最大公约数等于a除以b的余数c和b之间的最大公约数。

    public static int getGreatestCommonDivisor(int a, int b) {
        int max = Math.max(a, b);
        int min = Math.min(a, b);
        if (max % min == 0) {
            return min;
        }
        return getGreatestCommonDivisor(max % min, min);
    }

时间复杂度近似为O(log(max(a, b)),当两个正整数很大的时候,a % b的计算性能很差。

更相减损术

两个正整数a和b(a > b),它们的最大公约数等于a - b的差值c和较小数b的最大公约数。

    public static int getGreatestCommonDivisor(int a, int b) {
        if (a == b) {
            return a;
        }
        int max = Math.max(a, b);
        int min = Math.min(a, b);
        return getGreatestCommonDivisor(max - min, min);
    }

避免了大数取模的计算,但是算法性能不稳定,最坏的时间复杂度为O(max(a, b))

更优算法

  • 当a和b均为偶数时,gcd(a,b) = 2 * gcd(a / 2, b / 2) = 2 * gcd(a >> 1, b >> 1) = gcd(a >> 1, b >> 1) << 1;
  • 当a为偶数,b为奇数时,gcd(a, b) = gcd(a / 2, b) = gcd(a >> 1, b);
  • 当a为奇数,b为偶数时,gcd(a, b) = gcd(a, b / 2) = gcd(a, b >> 1);
  • 当a和b均为奇数时,先利用更相减损术运算一次,gcd(a, b) = gcd(b, a - b),此时a - b必是偶数,然后进行上面情况的运算。
    public static int getGreatestCommonDivisor(int a, int b) {
        if (a == b) {
            return a;
        }
        if ((a & 1) == 0 && (b & 1) == 0) {
            return getGreatestCommonDivisor(a >> 1, b >> 1) << 1;
        } else if ((a & 1) == 0 && (b & 1) != 0) {
            return getGreatestCommonDivisor(a >> 1, b);
        } else if ((a & 1) != 0 && (b & 1) == 0) {
            return getGreatestCommonDivisor(a, b >> 1);
        } else {
            int max = Math.max(a, b);
            int min = Math.min(a, b);
            return getGreatestCommonDivisor(max - min, min);
        }
    }

进行简化之后为

    public static int getGreatestCommonDivisor(int a, int b) {
        if (a == b) {
            return a;
        }
        if ((a & 1) == 0 && (b & 1) == 0) {
            return getGreatestCommonDivisor(a >> 1, b >> 1) << 1;
        } else if ((a & 1) == 0) {
            return getGreatestCommonDivisor(a >> 1, b);
        } else if ((b & 1) == 0) {
            return getGreatestCommonDivisor(a, b >> 1);
        } else {
            int max = Math.max(a, b);
            int min = Math.min(a, b);
            return getGreatestCommonDivisor(max - min, min);
        }
    }

这是更相减损法与位运算的结合方法,算法性能稳定,而且使用了位运算,时间复杂度为O(log(max(a, b)))

判断一个数是否为2的整数次幂

判断一个正整数是否为2的整数次幂

枚举

计算出2的整数次幂,依次递增,若是出现了大于该数的2次幂(并没有等于的),就证明该数不是2的整数次幂。

class Solution {
    public boolean isPowerOfTwo(int n) {
        int num = 1;
        while (num <= n) {
            if (num == n) return true;
            num = num << 1;
        }
        return false;
    }
}

算法的时间复杂度为O(logn)。

n & (n - 1) == 0

每一个2的整数次幂转换为二进制,都是1开头后面都是0的数串。

将该整数次幂减一,就得到二进制全为1的数串。

这两个数取按位与运算,就会出现每一位数都不一样,所以结果为0。

所以不为0的就不是2的整数次幂了。

class Solution {
    public boolean isPowerOfTwo(int n) {
        return (n & n - 1) == 0;
    }
}

n & -n == n

n & -n可以得到最低位的1的位置,即最低位为1其余全是0的数串,如果是2的整数次幂的话,那么该数串其实表示的就是本身。

class Solution {
    public boolean isPowerOfTwo(int n) {
        return (n & -n) == n;
    }
}

无序数组排序后的最大相邻差

有一个无序整数型数组,如何求出该数组排序后的任意两个相邻元素的最大差值?

排序后求最大差值

    public static int getMaxSortedDistance(int[] array) {
        Arrays.sort(array);
        int max = -1;
        for (int i = 1; i < array.length; i++) {
            int temp = array[i] - array[i - 1];
            if (temp > max) {
                max = temp;
            }
        }
        return max;
    }

将数组进行快速排序O(nlogn),然后进行遍历O(n),所以总的时间复杂度为O(nlogn)

空间复杂度就是该数组O(n)

计数排序

使用计数排序,找出新数组最多连续出现0的次数加1,就是最大相邻差了。

当数组的数差值不是很大的情况下,效率很高,但是差值很大的情况,数组的长度会很大。

    public static int getMaxSortedDistance(int[] array) {
        // 求出数组的最大值和最小值
        int max = array[0];
        int min = array[0];
        for (int i = 1; i < array.length; i++) {
            if (array[i] > max) {
                max = array[i];
            }
            if (array[i] < min) {
                min = array[i];
            }
        }
        int d = max - min;
        // 最大值和最小值相等,证明数组的所有数相等
        if (d == 0) {
            return 0;
        }
        // 计数排序
        int[] arr = new int[d + 1];
        for (int i : array) {
            arr[i - min]++;
        }
        int maxDistance = 0;
        
        // 寻找最长连续的0
        for (int i = 0; i < arr.length; i++) {
            if (arr[i] == 0) {
                int temp = i;
                while (arr[temp] == 0) {
                    temp++;
                }
                if (temp - i > maxDistance) {
                    maxDistance = temp - i;
                }
            }
        }
        return maxDistance + 1;
    }

桶排序

记录桶内的最大和最小值,统计出每一个桶的最大值,和这个桶右侧非空桶的最小值的差,数值最大的差即为题目所求,故时间复杂度为O(n)

    public static int getMaxSortedDistance(int[] array) {
        // 求出数组的最大值和最小值
        int max = array[0];
        int min = array[0];
        for (int i = 1; i < array.length; i++) {
            if (array[i] > max) {
                max = array[i];
            }
            if (array[i] < min) {
                min = array[i];
            }
        }
        int d = max - min;
        // 最大值和最小值相等,证明数组的所有数相等
        if (d == 0) {
            return 0;
        }

        // 初始化桶
        int bucketNum = array.length;
        Bucket[] buckets = new Bucket[bucketNum];
        Arrays.fill(buckets,new Bucket());

        // 遍历原始数组,确定每个桶的最大值和最小值
        for (int i : array) {
            int index = ((i - min) * (bucketNum - 1) / d);
            if (buckets[index].min == null || buckets[index].min > i) {
                buckets[index].min = i;
            }
            if (buckets[index].max == null || buckets[index].max < i) {
                buckets[index].max = i;
            }
        }

        // 遍历桶,找到最大差值
        int leftMax = buckets[0].max;
        int maxDistance = 0;
        for (int i = 1; i < buckets.length; i++) {
            if (buckets[i].min == null) {
                continue;
            }
            if (buckets[i].min - leftMax > maxDistance) {
                maxDistance = buckets[i].min - leftMax;
            }
            leftMax = buckets[i].max;
        }
        return maxDistance;
    }
    
    private static class Bucket {
        Integer min;
        Integer max;
    }

用栈实现队列

用栈来模拟一个队列,要求实现队列的两个基本操作:入队和出队。

使用一个栈明显是不能实现队列的,所以需要一个辅助栈。设这两个栈为a和b。

a栈用来存储入队的元素,入队的元素直接压入a栈就好。

b栈用来存储出队的元素,出队的元素就直接推出b栈就好。

当b栈是空的,就将a栈的所有元素出栈压入a栈,这样出栈的顺序就相反了,即符合先进先出了。

class CQueue {

    Stack<Integer> stack1;
    Stack<Integer> stack2;

    public CQueue() {
        stack1 = new Stack<>();
        stack2 = new Stack<>();
    }
    
    public void appendTail(int value) {
        stack1.push(value);
    }
    
    public int deleteHead() {
        if(!stack2.isEmpty()) {
            return stack2.pop();
        }
        if(stack1.isEmpty()) {
            return -1;
        }
        while(!stack1.isEmpty()) {
            stack2.push(stack1.pop());
        }
        return stack2.pop();
    }
    
}

/**
 * Your CQueue object will be instantiated and called as such:
 * CQueue obj = new CQueue();
 * obj.appendTail(value);
 * int param_2 = obj.deleteHead();
 */

删除k个数字后的最小值

给出一个整数,从该整数中去掉k个数字,要求剩下的数字形成的新整数尽可能小。应该如何选取被去掉的数字?

从左往右,第一位大于它右边的数,将该数删了就会让整个数值降低。

    public static String removeKDigits(String num, int k) {
        int newLength = num.length() - k;
        char[] stack = new char[num.length()];
        int top = 0;
        for (int i = 0; i < num.length(); i++) {
            char c = num.charAt(i);
            while (top > 0 && stack[top - 1] > c && k > 0) {
                top -= 1;
                k -= 1;
            }
            stack[top++] = c;
        }
        int offset = 0;
        while (offset < newLength && stack[offset] == '0') {
            offset++;
        }
        return offset == newLength ? "0" : new String(stack,offset,newLength - offset);
    }

如何实现大整数相加

给出两个很大的正整数,要求实现程序求出两个整数之和。

    public static String bigNumberSum(String bigNumberA, String bigNumberB) {
        int maxLen = Math.max(bigNumberA.length(), bigNumberB.length()) + 1;
        // 将两个大数逆序存储到数组中
        int[] arrA = new int[maxLen]; 
        for (int i = 0; i < bigNumberA.length(); i++) {
            arrA[i] = bigNumberA.charAt(bigNumberA.length() - i - 1) - '0';
        }
        int[] arrB = new int[maxLen];
        for (int i = 0; i < bigNumberB.length(); i++) {
            arrB[i] = bigNumberB.charAt(bigNumberB.length() - i - 1) - '0';
        }
        // 进行运算
        int[] res = new int[maxLen];
        for (int i = 0; i < res.length; i++) {
            int temp = res[i];
            temp += arrA[i] + arrB[i];
            if (temp >= 10) {
                temp -= 10;
                res[i + 1]++;
            }
            res[i] = temp;
        }
        
        // 将结果逆序
        StringBuilder str = new StringBuilder();
        boolean findFirst = false;
        for (int i = res.length - 1; i >= 0; i--) {
            if (!findFirst) {
                if (res[i] == 0) {
                    continue;
                }
                findFirst = true;
            }
            str.append(res[i]);
        }
        return str.toString();
    }

金矿问题

​ 很久很久以前,有一位国王拥有5座金矿,每座金矿的黄金储量不同,需要参与挖掘的工人人数也不同。例如又得金矿储量是500kg黄金,需要5个工人来挖掘;有的金矿储量是200kg黄金,需要3个工人来挖掘…

​ 如果参与挖矿的工人的总数是10.每座金矿要么全挖,要么不挖,不能派出一半人挖取一半的金矿。要用程序求出,要想得到尽可能多的黄金,应该选取挖取那几座金矿?

这题就是一道背包问题,根据题意可知这是01背包的问题,有两种状态转移的情况,选或者是不选。


    /**
     * @param w 工人数量
     * @param p 金矿开采所需的工人数量
     * @param g 金矿储量
     */
    public static int getBestGoldMining(int w, int[] p, int[] g) {
        int[] res = new int[w + 1];
        for (int i = 1; i <= g.length; i++) {
            for (int j = w; j >= 1; j--) {
                if (j >= p[i - 1]) {
                    res[j] = Math.max(res[j],res[j - p[i - 1]] + g[i - 1]);
                }
            }
        }
        return res[w];
    }

寻找缺少的整数

在一个无序数组里有99个不重复的正整数,范围从1到100,唯独缺少1个1~100中的整数。如何找出整个缺少的整数。

哈希表

创建一个哈希表,将1到100这100个数全部存入key中,遍历整个数组,将存在的key都删掉,剩下的key就是缺少的整数。

    public static int getBestGoldMining(int[] arr) {
        Map<Integer, Integer> map = new HashMap<>(100);
        for (int i = 1; i <= 100; i++) {
            map.put(i,i);
        }
        for (int i : arr) {
            map.remove(i);
        }
        int res = 0;
        for (int key : map.keySet()) {
          res = key;  
        }
        return res;
    }

先排序后寻找

将数组进行排序,然后逐个寻找。

    public static int getBestGoldMining(int[] arr) {
        Arrays.sort(arr);
        if (arr[0] != 1) {
            return 0;
        }
        for (int i = 0; i < arr.length - 1; i++) {
            if (arr[i] + 1 != arr[i + 1]) {
                return arr[i] + 1;
            }
        }
        return 100;
    }

求和

计算出1~100的和,然后减去数组中元素的和,剩下的就是所缺少的整数了。

    public static int getBestGoldMining(int[] arr) {
        int res = 5050;
        for (int i : arr) {
            res -= i;
        }
        return res;
    }

求只出现一次的数

​ 一个无序数组里有若干个正整数,范围是1~100,其中99个整数都出现了偶数次,只有一个整数出现了奇书次,如何找到这个出现奇数次的整数?

可以将数与数出现的次数当成Key和Value放入哈希表,然后寻找出现次数为1的,和上述哈希表的做法差不多,就不深入了。

这里可以使用异或运算,先了解异或运算的规律。

  1. 两个相同的数异或,结果等于0
  2. 任何数与0异或,结果等于本身
  3. 异或运算符合交换律

故将数组中的数进行异或,偶数次的数异或会变成0,奇数次的数就会留着。

    public static int getBestGoldMining(int[] arr) {
        int res = 0;
        for (int i : arr) {
            res ^= i;
        }
        return res;
    }

LRU(最近最少使用算法)

package com.yww;

import java.util.HashMap;

/**
 * @ClassName LRUCache
 * @author yww
 */
public class LRUCache {

    /**
     * 头节点
     */
    private Node head;
    /**
     * 尾节点
     */
    private Node end;
    /**
     * 缓存的个数
     */
    private int limit;
    /**
     * 哈希表缓存
     */
    private HashMap<String, Node> map;

    class Node {
        public Node pre;
        public Node next;
        public String key;
        public String value;
        Node(String key, String value) {
            this.key = key;
            this.value = value;
        }
    }

    public LRUCache(int limit) {
        this.limit = limit;
        map = new HashMap<>();
    }

    /**
     * 通过key获取缓存节点
     */
    public String get(String key) {
        Node node = map.get(key);
        if (node == null) {
            return null;
        }
        refreshNode(node);
        return node.value;
    }

    /**
     * 放入缓存,若key存在,更新value值
     */
    public void put(String key, String value) {
        Node node = map.get(key);
        if (node == null) {
            if (map.size() >= limit) {
                String oldKey = removeNode(head);
                map.remove(oldKey);
            }
            node = new Node(key, value);
            addNode(node);
            map.put(key,node);
        } else {
            node.value = value;
            refreshNode(node);
        }
    }

    /**
     * 通过key删除缓存
     */
    public void remove(String key) {
        Node node = map.get(key);
        removeNode(node);
        map.remove(key);
    }

    /**
     * 刷新被访问的节点
     */
    private void refreshNode(Node node) {
        if (node == end) {
            return;
        }
        removeNode(node);
        addNode(node);
    }

    /**
     * 删除节点
     */
    private String removeNode(Node node) {
        if (node == head && node == end) {
            head = null;
            end = null;
        } else if (node == end) {
            end = end.pre;
            end.next = null;
        } else if (node == head) {
            head = head.next;
            head.pre = null;
        } else {
            node.pre.next = node.next;
            node.next.pre = node.pre;
        }
        return node.key;
    }

    /**
     * 尾部插入节点
     */
    private void addNode(Node node) {
        if (end != null) {
            end.next = node;
            node.pre = end;
            node.next = null;
        }
        end = node;
        if (head == null) {
            head = node;
        }
    }

}