左神刷题


title: 左神刷题
date: 2018-09-21 17:32:47
updated: 2020-02-22 14:41:00
categories: 算法刷题
tags:
- 算法刷题


此文档包含左神书上题目(未完结,只做了部分)以及所涉及的leetcode题目,尽量都留存了最优解,部分简单的题目没有进行总结。左神的书——《程序员代码面试指南》

一、题目总览

编号 题目
1 按照左右半区的方式重新组合单链表
2 用递归函数和栈操作逆序栈
3 猫狗队列
4 用一个栈来实现另一个栈的排序
5 汉诺塔问题
6 构造数组的MaxTree
7 最大子矩阵(直方图-栈)
8 找两个排序数组的中位数
9 删除链表中的节点(两道)
10 找到/删除链表的中间节点(两道)
11 删除链表a/b处的节点
12 判断一个链表是否为回文结构
13 将单链表按某值划分成左边小,中间相等,右边大
14 单链表的归并排序
15 单链表的快速排序
16 单链表的选择排序
17 两个链表生成相加链表
18 删除单链表的重复节点(三道)
19 向有序的环形单链表中插入新节点
20 打印二叉树的边界节点
21 编辑距离
22 找出数组中左边比他小右边比他大的所有元素
23 判断二叉树为BST(二叉搜索树)
24 Replace Words(字典树应用)
25 BFS/DFS思想
26 BFS相关题目(1道)
27 DFS相关题目(4道)
28 图的几种最短路算法(4道)
29 Word Ladder(2道)(SPFA最短路算法应用)
30 字符串匹配(kmp算法)
31 两数相除(转为减法)
32 sqrt(x)
33 乱序的数组中找到最长的递增子序列
34 蓄水池问题
35 满足指定sum条件的长度最小的子数组
36 最大值减去最小值小于或等于num的子数组数量
37 几道未整理的

二、题目及解答

1.按照左右半区的方式重新组合单链表

《左神》86、牛客网有编程挑战题

给定一个单链表的头部节点head,链表长度为N。 如果N为偶数,那么前N/2个节点算作左半区,后N/2个节点算作右半区; 如果N为奇数,那么前N/2个节点算作左半区,后N/2+1个节点算作右半区; 左半区从左到右依次记为L1->L2->…,右半区从左到右依次记为R1->R2->…。请将单链表调整成L1->R1->L2->R2->…的样子。

1->2->3->4 调整后:1->3->2->4 
1->2->3->4->5 调整后:1->3->2->4->5 

要求:如果链表长度为N,时间复杂度请达到O(N),额外空间复杂度请达到O(1)

思路:遍历两遍链表,先找到左半区的最后一个节点,再左右半区重新拼接链表。时间O(N),空间O(1)

public static void relocateList(ListNode head) {
    if(head==null || head.next==null || head.next.next==null || head.next.next.next==null){
        return;
    }

    ListNode guard = new ListNode(0);//哨兵
    guard.next = head;
    ListNode left = guard.next;//左半区
    ListNode right;//左半区的最后一个节点

    //1.遍历一遍链表,找到左半区的最后一个节点
    ListNode fast = guard;
    ListNode low = guard;
    while(fast.next!=null && fast.next.next!=null){//fast走两步,low走一步
        fast = fast.next.next;
        low = low.next;
    }
    right = low;//找到了左半区的最后一个节点

    //2.再左右半区重新拼接链表
    ListNode rightCopy = right;//缓存左半区的最后一个节点
    while(left!=rightCopy){
        //1.剥离当前节点
        ListNode temp = right.next;
        right.next = temp.next;
        temp.next = null;

        //2.拼接左右半区节点
        temp.next = left.next;
        left.next = temp;
        left = left.next.next;
    }
}

2.用递归函数和栈操作逆序栈

《左神》8

题目:一个栈依次压入1,2,3,4,5那么从栈顶到栈底分别为5,4,3,2,1。将这个栈转置后,从栈顶到栈底为1,2,3,4,5,也就是实现了栈中元素的逆序,请设计一个算法实现逆序栈的操作,但是只能用递归函数来实现,而不能用另外的数据结构。

思路:两个递归函数搞定

import java.util.Stack;
//用递归函数和栈操作逆序栈
public class ReverseStack {
    //逆序栈元素
    public void reverseStackRecursively(Stack<Integer> stack) {
        if(stack.isEmpty()){
            return;
        }
        int bottom = getAndDelBottom(stack);
        reverseStackRecursively(stack);
        stack.push(bottom);
    }
    //删除并返回栈底元素
    public int getAndDelBottom(Stack<Integer> stack){
        int curData = stack.pop();
        if(stack.isEmpty()){
            return curData;
        }
        else{
            int last = getAndDelBottom(stack);
            stack.push(curData);
            return last;
        }
    }
}

3.猫狗队列

《左神》10

题目:实现一种狗猫队列的结构,要求如下:

(1)用户可以调用add方法将cat类或dog类的实例放入队列中;

(2)用户可以调用pollAll方法,将队列中所有的实例按照进队列的先后顺序依次弹出;

(3)用户可以调用pollDog方法,将队列中dog类的实例按照进队列的先后顺序依次弹出;

(4)用户可以调用pollCat方法,将队列中cat类的实例按照进队列的先后顺序依次弹出;

(5)用户可以调用isEmpty方法,检查队列中是否还有dog或cat的实例;

(6)用户可以调用isDogEmpty方法,检查队列中是否还有dog类的实例;

(7)用户可以调用isCatEmpty方法,检查队列中是否还有cat类的实例。

思路:新定义带时间戳的Pet类(PetEnterQue),然后用两个队列(dogQue\catQue)结合时间戳来定义新类(dogCatQue),猫狗队列就是这个类的一个实例

import java.util.LinkedList;
import java.util.Queue;
/**
 * 猫狗队列。
 */
public class Pet {
    private String type;
    public Pet(String type) {
        this.type = type;
    }
    public String getPetType() {
        return type;
    }
}
	
public class Dog extends Pet{
    public Dog() {
        super("dog");
    }
}
	
public class Cat extends Pet{
    public Cat() {
        super("cat");
    }
}
	
//新定义一个带时间戳的Pet类
public class PetEnterQue{
    private Pet pet;
    private long count; //时间戳
	
    public PetEnterQue(Pet pet, long count) {
        this.pet = pet;
        this.count = count;
    }
	
    public Pet getPet() {
        return pet;
    }
	
    public long getCount() {
        return count;
    }
}
	
//这个类的实例就是一个猫狗队列
public class dogCatQue{
    private Queue<PetEnterQue> dogQue;
    private Queue<PetEnterQue> catQue;
    private long count;
    public dogCatQue(){
        dogQue = new LinkedList<>();
        catQue = new LinkedList<>();
        count = 0;
    }
	
    public void offer(Pet pet){
        if(pet.getPetType().equals("dog")){
            dogQue.offer(new PetEnterQue(pet, count++));
        }
        else{
            catQue.offer(new PetEnterQue(pet, count++));
        }
    }
	
    public Pet pollDog(){
        if(dogQue.isEmpty()){
            return null;
        }
        return dogQue.poll().getPet();
    }
	
    public Pet pollCat(){
        if(catQue.isEmpty()){
            return null;
        }
        return catQue.poll().getPet();
    }
	
    public Pet pollAll(){
        if(dogQue.isEmpty()){
            if(!catQue.isEmpty()){
                return catQue.poll().getPet();
            }
        }
        else if(catQue.isEmpty()){
            if(!dogQue.isEmpty()){
                return dogQue.poll().getPet();
            }
        }
        else{//比较时间戳,将早进队列的Pet出队
            return dogQue.peek().getCount()>catQue.peek().getCount()? catQue.poll().getPet() :dogQue.poll().getPet();
        }
        return null;
    }
	
    public boolean isEmpty(){
        return dogQue.isEmpty() && catQue.isEmpty();
    }
	
    public boolean isDogEmpty(){
        return dogQue.isEmpty();
    }
	
    public boolean isCatEmpty(){
        return catQue.isEmpty();
    }
}

4.用一个栈来实现另一个栈的排序

《左神》13

题目:在一个栈中元素的类型为整型,现在想将该栈从栈顶到栈底按从大到小的顺序排序,只许申请一个栈,除此之外,可以申请其他变量,但是不能申请额外的数据结构。

//用一个栈来实现另一个栈的排序
public static void sortStackByStack(Stack<Integer> stack){
    Stack<Integer> help = new Stack<>();
    while(!stack.isEmpty()){
        int temp = stack.pop();
        while(!help.isEmpty() && temp>help.peek()){
            stack.push(help.pop());
        }
        help.push(temp);
    }

    while(!help.isEmpty()){
        stack.push(help.pop());
    }
}

public static void main(String[] args){
    Stack<Integer> stack = new Stack<>();
    stack.push(4);
    stack.push(3);
    stack.push(76);
    stack.push(5);
    stack.push(8);
    stack.push(9);
    System.out.println(stack);
    sortStackByStack(stack);
    System.out.println(stack);
}

5.汉诺塔问题(两道)

1.正常汉诺塔:可以直接从左移到右,不需经过中间

法一递归(常用这个):

//汉诺塔tower of hanoi问题。法一:递归
public static void towerOfHanoi(int n) {
    move("left", "right", "mid", n);
}

public static void move(String start, String end, String buffer, int n){
    if(n==1){
        System.out.println("from " + start + " to " + end);
        return;
    }
    move(start, buffer, end, n-1); //将1-n-1移动到buffer,即缓冲区
    System.out.println("from " + start + " to " + end); //将n移动到目标柱子上
    move(buffer, end, start, n-1); // 将1-n-1移动到目标柱子上
}

public static void main(String[] args){
    towerOfHanoi(3);
    //out:
    //from left to right
    //from left to mid
    //from right to mid
    //from left to right
    //from mid to left
    //from mid to right
    //from left to right
}

法二用栈:

public class HanoiStack {
    public static void main(String[] args) {
        Stack hanoi = new Stack();
        hanoi.push(new Problem(4, 'A', 'B', 'C'));
        Problem myProblem = null;
        while (!hanoi.isEmpty() && (myProblem = (Problem) hanoi.pop()) != null) {
            if (myProblem.n == 1) {
                System.out.println(myProblem.A+"->"+myProblem.C);
            } else {
                hanoi.push(new Problem(myProblem.n-1, myProblem.B, myProblem.A, myProblem.C));
                hanoi.push(new Problem(1, myProblem.A, myProblem.B, myProblem.C));
                hanoi.push(new Problem(myProblem.n-1, myProblem.A, myProblem.C, myProblem.B));
            }
        }
    }
}
class Problem {
    int n;
    char A, B, C;
    public Problem(int n, char A, char B, char C) {
        this.n = n;
        this.A = A;
        this.B = B;
        this.C = C;
    }
}

2.特殊汉诺塔:不可以直接从左移到右,必须经过中间

《左神》14

法一递归:

//法一:递归。
public static int hanoiProblem1(int num, String left, String mid, String right){
    if(num<1){
        return 0;
    }
    return process(num, left, mid, right, left, right);
}
public static int process(int num, String left, String mid, String right, String from, String to){
    if(num==1){
        if(from.equals(mid) || to.equals(mid)){
            System.out.println("move 1 from " + from + " to " + to);
            return 1;
        }
        else{
            System.out.println("move 1 from " + from + " to mid");
            System.out.println("move 1 from mid to " + to);
            return 2;
        }
    }
    else{
        if(from.equals(mid) || to.equals(mid)){
            String another = (from.equals(left)||to.equals(left))? right : left;
            int step1 = process(num-1, left, mid, right, from, another);
            System.out.println("move " + num + " from " + from + " to " + to);
            int step2 = 1;
            int step3 = process(num-1, left, mid, right, another, to);
            return step1 + step2 + step3;
        }
        else{
            int step1 = process(num-1, left, mid, right, from, to);
            int step2 = 1;
            System.out.println("move " + num + " from " + from + " to mid");
            int step3 = process(num-1, left, mid, right, to, from);
            int step4 = 1;
            System.out.println("move " + num + " from mid to " + to);
            int step5 = process(num-1, left, mid, right, from, to);
            return step1 + step2 + step3 + step4 + step5;
        }
    }
}

法二:栈。

非递归的方法核心思想:

1.由于必须经过中间,把三个柱子想成三个栈,每次操作栈顶的一个元素,只有四个动作L->M、M->L、M->R、R->M

2.最优步骤时,每次四个动作只有一个动作能同时满足3、4两个原则,因为满足3、4时直接进行该步骤即可,直到最终第三个栈元素都移过去结束

3.每次移动元素时,栈顶元素小压大

4.每次移动元素时,与上一次移动操作不可互逆(那就是重复无意义的操作)

import java.util.Stack;
enum Action{
    No, LToM, MToL, RToM, MToR
}
public class Solution2{
    //法二:栈
    public static int hanoiProblem2(int num, String left, String mid, String right){
        if(num<1){
            return 0;
        }
        Stack<Integer> ls = new Stack<>();
        Stack<Integer> ms = new Stack<>();
        Stack<Integer> rs = new Stack<>();
        ls.push(Integer.MAX_VALUE);
        ms.push(Integer.MAX_VALUE);
        rs.push(Integer.MAX_VALUE);
        for(int i=num; i>=1; i--){
            ls.push(i);
        }
	
        Action[] record = {Action.No};//record[0]存储上一次操作
        int res = 0;
        while(rs.size()!=num+1){
            res += fstack_To_tStack(record, Action.LToM, Action.MToL, ls, ms, left, mid);
            res += fstack_To_tStack(record, Action.MToL, Action.LToM, ms, ls, mid, left);
            res += fstack_To_tStack(record, Action.RToM, Action.MToR, rs, ms, right, mid);
            res += fstack_To_tStack(record, Action.MToR, Action.RToM, ms, rs, mid, right);
        }
        return res;
    }
    private static int fstack_To_tStack(Action[] record, Action nowAct, Action nowActReverse, Stack<Integer>fstack, Stack<Integer>tstack, String from, String to){
        if(record[0]!=nowActReverse && fstack.peek()<tstack.peek()){//和上次操作不互逆 且 栈顶满足小压大,即为当前应该走的一步
            tstack.push(fstack.pop());
            System.out.println("move " + tstack.peek() + " from " + from + " to " + to);
            record[0] = nowAct;
            return 1;
        }
        return 0;
    }
	
    public static void main(String[] args){
//        System.out.println(hanoiProblem1(2,"left", "mid", "right"));//法一:递归
        System.out.println(hanoiProblem2(2,"left", "mid", "right"));//法二:栈
    }
}

6.构造数组的MaxTree

leetcode 654、《左神》22

题目:对于一个没有重复元素的整数数组,请用其中元素构造一棵MaxTree。MaxTree定义为一棵二叉树,其中的节点与数组元素一一对应,同时对于MaxTree的每棵子树,它的根的元素值为子树的最大值。

思路:现有一建树方法,对于数组中的每个元素,其在树中的父亲为数组中它左边比它大的第一个数和右边比它大的第一个数中更小的一个。若两边都不存在比它大的数,那么它就是树根。请设计O(n)的算法实现这个方法。

核心转化为了怎么找到左边/右边第一个比它大的数呢?用栈,递减序列即可。

以找每个数左边第一个比他大的数为例,从左到右遍历每个数,栈中保持递减序列,新来的数不停的Pop出栈顶直到栈顶比新数大或没有数。以[3,1,2]为例,首先3入栈,接下来1比3小,无需pop出3,1入栈,并且确定了1往左第一个比他大的数为3。接下来2比1大,1出栈,2比3小,2入栈。并且确定了2往左第一个比他大的数为3。用同样的方法可以求得每个数往右第一个比他大的数。时间复杂度O(n),空间复杂度也是O(n)为最优解法。

本题最巧妙的一点是如何找某数的左边最近的比它大的值和右边最近的比它大的值。取左右两边的较小的数作为该数的父节点。

import java.util.Stack;
class Node{
    public int value;
    public Node left;
    public Node right;
    public Node(int data){
        this.value = data;
    }
}
public class Solution {
    //构造数组的MaxTree。核心:如何找某数的左边最近的比它大的值和右边最近的比它大的值(辅助栈和数组)。取左右两边的较小的数作为该数的父节点。
    public static Node makeMaxTree(int[] arr) {
        if(arr==null || arr.length==0){
            return null;
        }
        if(arr.length==1){
            return new Node(arr[0]);
        }

        Stack<Integer> stack = new Stack<>();
        int[] lmax = new int[arr.length];//存储找出某数的左边最近的比它大的值的索引
        int[] rmax = new int[arr.length];//存储找出某数的右边最近的比它大的值的索引
        for(int i=0; i<arr.length; ++i){//从左往右遍历,找出某数的左边最近的比它大的值。栈递减序列
            while(!stack.isEmpty() && arr[stack.peek()]<arr[i]){
                stack.pop();
            }
            lmax[i] = !stack.isEmpty() ? stack.peek() : -1;//左边最近的比它大的值,如果左边没有比它大的,则为-1
            stack.push(i);
        }
        stack.clear();//清空栈
        for(int i=arr.length-1; i>=0; --i){//从右往左遍历,找出某数的右边最近的比它大的值。栈递减序列
            while(!stack.isEmpty() && arr[stack.peek()]<arr[i]){
                stack.pop();
            }
            rmax[i] = !stack.isEmpty() ? stack.peek() : -1;//左边最近的比它大的值,如果左边没有比它大的,则为-1
            stack.push(i);
        }

        //两个数组(左边首个最大、右边首个最大)存储好了,取较小的节点作为父节点,开始构建maxTree
        Node[] nodes = new Node[arr.length];
        for(int i=0; i<nodes.length; ++i){
            nodes[i] = new Node(arr[i]);
        }
        Node head = null;
        for(int i=0; i<arr.length; ++i){
            if(lmax[i]==-1 && rmax[i]==-1){
                head = nodes[i];
            }
            else if(lmax[i]==-1 && rmax[i]!=-1){
                if(nodes[rmax[i]].left == null){
                    nodes[rmax[i]].left = nodes[i];
                }
                else{
                    nodes[rmax[i]].right = nodes[i];
                }
            }
            else if(rmax[i]==-1 && lmax[i]!=-1){
                if(nodes[lmax[i]].right == null){
                    nodes[lmax[i]].right = nodes[i];
                }
                else{
                    nodes[lmax[i]].left = nodes[i];
                }
            }
            else if(arr[lmax[i]]<arr[rmax[i]]){
                if(nodes[lmax[i]].right == null){
                    nodes[lmax[i]].right = nodes[i];
                }
                else{
                    nodes[lmax[i]].left = nodes[i];
                }
            }
            else if(arr[lmax[i]]>arr[rmax[i]]){
                if(nodes[rmax[i]].left == null){
                    nodes[rmax[i]].left = nodes[i];
                }
                else{
                    nodes[rmax[i]].right = nodes[i];
                }
            }
        }

        return head;
    }

    public static void main(String[] args){
        int[] arr = new int[]{3,1,2};
        System.out.println(makeMaxTree(arr));
    }
}

7.最大子矩阵(2道)(直方图-单调栈)

1.Largest Rectangle in Histogram(直方图面积)--leetcode 84
2.Maximal Rectangle(最大子矩阵大小)--leetcode85、《左神》26

1.Largest Rectangle in Histogram(直方图面积)

leetcode 84

题目:Given n non-negative integers representing the histogram’s bar height where the width of each bar is 1, find the area of largest rectangle in the histogram.

Above is a histogram where width of each bar is 1, given height = [2,1,5,6,2,3]. out: 10

思路:辅助单调栈,递增存储。遍历原直方图数组,递增往栈里存储,如果遇到比栈顶小的元素时,循环栈顶弹出并计算resMax,直到继续递增存储。时间o(n),空间o(n)

public int largestRectangleArea(int[] heights) {
    if(heights==null || heights.length==0){
        return 0;
    }
    if(heights.length==1){
        return heights[0];
    }
    Stack<Integer> stack = new Stack<>();//单调栈,递增存储,每次遇到比栈顶小的元素时,对栈进行判断
    int resMax = 0;
    for(int i=0; i<heights.length; ++i){
        while(!stack.isEmpty() && heights[i]<heights[stack.peek()]){//每次遇到比栈顶小的元素时,对栈进行判断
            int j = stack.pop();
            int k = stack.isEmpty() ? -1 : stack.peek();
            resMax = Math.max(resMax, (i-k-1)*heights[j]);
        }
        stack.push(i);//递增存储
    }

    while(!stack.isEmpty()){//考虑[1,2,3]的情况,栈一直递增存储,最后必须要清空栈
        int j = stack.pop();
        int k = stack.isEmpty() ? -1 : stack.peek();
        resMax = Math.max(resMax, (heights.length-k-1)*heights[j]);
    }
    return resMax;
}

2.Maximal Rectangle(最大子矩阵大小)

leetcode85、《左神》26

题目:Given a 2D binary matrix filled with 0’s and 1’s, find the largest rectangle containing only 1’s and return its area.

1 0 1 0 0
1 0 1 1 1
1 1 1 1 1
1 0 0 1 0
Return 6.
	

思路:一行一行进行,对矩阵每一行为底的直方图数组求解直方图最大面积,直方图最大面积:用单调递增栈。

public int maximalRectangle(char[][] matrix) {
    if(matrix==null || matrix.length==0){
        return 0;
    }
    int m = matrix.length;
    int n = matrix[0].length;
    if(n==0){
        return 0;
    }

    int res = 0;
    int[] temp = new int[n+1];//以矩阵每一行为底的直方图数组,最后一个元素temp[n]要为0,便于求解直方图面积
    for(int i=0; i<m; ++i){//一行一行处理直方图的最大面积
        for(int j=0; j<n; ++j){//每一行先更新直方图
            if(matrix[i][j] == '0'){
                temp[j] = 0;
            }else{
                temp[j]++;
            }
        }
        res = Math.max(res, largeArea(temp)); //直方图最大面积
    }
    return res;
}

//求一行的直方图最大面积--单调递增栈
public int largeArea(int[] heights){
    if(heights.length==1){
        return heights[0];
    }
    Stack<Integer> stack = new Stack<>();//单调栈,递增存储,每次遇到比栈顶小的元素时,对栈进行判断
    int resMax = 0;
    for(int i=0; i<heights.length; ++i){
        while(!stack.isEmpty() && heights[i]<heights[stack.peek()]){//每次遇到比栈顶小的元素时,对栈进行判断
            int j = stack.pop();
            int k = stack.isEmpty() ? -1 : stack.peek();
            resMax = Math.max(resMax, (i-k-1)*heights[j]);
        }
        stack.push(i);//递增存储
    }

    return resMax;
}

8.找两个排序数组的中位数

leetcode 4

题目:给定数组arr和整数num,共返回有多少个子数组满足子数组的最大值-最小值<=num

//Median of Two Sorted Arrays找两个排序数组的中位数。思路:二分法,核心是注意怎么二分和边界条件
public double findMedianSortedArrays(int[] nums1, int[] nums2) {
    int n = nums1.length;
    int m = nums2.length;
    int left = (n + m + 1) / 2;
    int right = (n + m + 2) / 2;
    return 0.5 * (getKth(nums1,0,n-1,nums2,0,m-1,left) + getKth(nums1,0,n-1,nums2,0,m-1,right));
}

private double getKth(int[] nums1, int start1, int end1, int[] nums2, int start2, int end2, int k){
    int len1 = end1 - start1 + 1;
    int len2 = end2 - start2 + 1;
    if(len1 > len2){
        return getKth(nums2,start2,end2,nums1,start1,end1,k);
    }
    if(len1 == 0) return nums2[start2 + k - 1];
    if(k == 1) return Math.min(nums1[start1],nums2[start2]);
    int s1 = Math.min(start1 + k/2 - 1, start1 + len1 - 1);
    int s2 = Math.min(start2 + k/2 - 1, start2 + len2 - 1);//两个数组的k/2位置上的数,如果数组长度小于k/2,直接比len-1上的数
    if(nums1[s1] < nums2[s2]){//nums1数组的前k/2个数不要了,下次k=k-k/2或者k=k-len1/2
        return getKth(nums1,s1+1,end1,nums2,start2,end2,k-Math.min(k/2,len1));
    }else{//nums2数组的前k/2个数不要了,下次k=k-k/2或者k=k-len1/2
        return getKth(nums1,start1,end1,nums2,s2+1,end2,k-Math.min(k/2,len2));
    }
}

9. 删除链表中的节点(两道)

1.删除某个链表中指定的(非末尾)节点--leetcode237、《左神》83
2.删除链表中等于给定值 val 的所有节点--leetcode203

1.删除某个链表中指定的(非末尾)节点,你将只被给定要求被删除的节点。

leetcode237、《左神》83

思路:后一个节点的val覆盖当前节点,删除后一个节点即可

/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode(int x) { val = x; }
 * }
 */
class Solution {
    public void deleteNode(ListNode node) {
        node.val = node.next.val;
        node.next = node.next.next;
    }
}

2.删除链表中等于给定值 val 的所有节点

leetcode203、《左神》73

输入: 1->2->6->3->4->5->6, val = 6
输出: 1->2->3->4->5

思路:遍历链表删除即可。

public ListNode removeElements(ListNode head, int val){
    if(head==null){
        return null;
    }
    ListNode resHead = new ListNode(0); //哨兵
    resHead.next = head;
    ListNode p = resHead;
    ListNode cur = p.next;
    while(cur!=null){
        if(cur.val==val){
            p.next = cur.next;//这种删除思想特别好
        }
        else{
            p = cur;
        }
        cur = cur.next;
    }
	
    return resHead.next;
}

10.找到/删除链表的中间节点(两道)

1.找到链表的中间节点--leetcode 876
2.删除链表的中间节点--《左神》38

1.找到链表的中间节点

leetcode 876

题目:Given a non-empty, singly linked list with head node head, return a middle node of linked list.

If there are two middle nodes, return the second middle node.

Input: [1,2,3,4,5]
Output: Node 3 from this list (Serialization: [3,4,5])
The returned node has value 3. 

[1,2,3,4,5,6]
Output: Node 4 from this list (Serialization: [4,5,6])

思路:两个指快慢指针走即可。

public ListNode middleNode(ListNode head) {
    if(head.next==null){
        return head;
    }
    ListNode right = head;
    ListNode left = head;
    while(right.next!=null && right.next.next!=null){
        right = right.next.next;
        left = left.next;
    }
    return right.next==null ? left : left.next;
}

2.删除链表的中间节点

《左神》38.

题目:删除一个链表的中间节点,当链表只有一个节点的时候或者head节点为空的时候返回head,当链表有两个节点的时候删除第一个节点,当链表有三个节点的时候删除第二个节点,当链表有四个节点的时候删除第二个节点,当链表有五个节点的时候删除第三个节点…

思路:一个链表长度每增加二,要删除的节点就后移一个节点,要删除一个节点需要知道它的前一个节点。

//删除链表中间节点。思路:两个快慢指针走即可
public static ListNode removeMidNode(ListNode head){
    if(head==null || head.next==null){//空节点或只有一个节点
        return head;
    }
    if(head.next.next==null){//两个节点,删除第一个节点
        head = head.next;
        return head;
    }
    ListNode right = head.next.next;
    ListNode left = head;//要删除节点的上一个节点
    while(right.next!=null && right.next.next!=null){
        right = right.next.next;
        left = left.next;
    }
    left.next = left.next.next;//删除中间节点
    return head;
}

11.删除链表a/b处的节点

《左神》38.

题目:给两个整数a,b(a<=b),实现删除链表a/b处节点的函数。若r=0,不删除;其他r的值向上取整,比如r在范围(2/5,3/5]中,取3,删除第三个节点。

思路:1.一次遍历求链表长度;2.求删第几个(向上取整);3.删除r节点

//删除链表a/b处的节点。思路:1.一次遍历求链表长度;2.求删第几个(向上取整);3.删除r节点
public static ListNode removeByRatio(ListNode head, int a, int b){
    if(a==0 || b==0 || a==b || head==null){//不删除任何节点
        return head;
    }
    //1.遍历链表,得其长度len
    ListNode p = head;
    int len = 0;
    while(p!=null){
        len++;
        p = p.next;
    }
    //2.计算a/b处是第k个节点
    int k = (int)Math.ceil( ((double)a/b)*len );
    if(k<0){//a/b为负数,不删除节点
        return head;
    }
    //3.删除第k个节点
    if(k==1){
        head = head.next;
        return head;
    }
    p = head;//从头遍历
    while(k!=2){//走到第k-1处
        p = p.next;
        k--;
    }
    p.next = p.next.next;//删除第k个节点
    return head;
}

12.判断一个链表是否为回文结构

《左神》48、leetcode234

题目:判断一个链表是否为回文链表(1\121\1221),要求时间o(n),空间o(1)

思路:由于要求时间o(n),空间o(1),感觉必须要动链表了。1.先找到中间节点,2.然后右半边逆序,3.然后分别从头部和中间开始比较元素是否相等即可。时间o(n),空间o(1)。

//判断一个链表是否为回文结构。思路:1.两个快慢指针先找到中间节点,2.然后右半边链表逆序,3.再从头遍历比较元素是否相等
public boolean isPalindrome(ListNode head) {
    if(head==null || head.next==null){
        return true;
    }
    //2.两个快慢指针先找到中间节点
    ListNode left = head;
    ListNode right = head;
    while(right.next!=null && right.next.next!=null){
        right = right.next.next;
        left = left.next;
    }
    //2.现将left后边的链表逆序
    ListNode cur = left.next;
    ListNode temp = cur;
    cur = cur.next;
    temp.next = null;
    while(cur!=null){
        temp = cur;
        cur = cur.next;
        temp.next = left.next;
        left.next = temp;
    }

    //3.从头遍历比较元素是否相等
    right = left.next;
    left = head;
    while(right!=null){
        if(left.val!=right.val){//遇到元素不等,不是回文结构,false
            return false;
        }
        left = left.next;
        right = right.next;
    }
    return true;
}
public static void main(String[] args){
    ListNode head = new ListNode(1);
    head.next = new ListNode(2);
    head.next.next = new ListNode(3);
    head.next.next.next = new ListNode(3);
    head.next.next.next.next = new ListNode(2);
    head.next.next.next.next.next = new ListNode(1);
    System.out.println(isPalindrome(head));//true
}

13.将单向链表按某值划分成左边小,中间相等,右边大

《左神》52.

题目:给定一个单向链表的头结点head,节点的值类型是整型,再给定一个整数privot。实现一个调整链表的函数,将链表调整为左部分都是值小于privot的节点,中间部分都是值等于privot的节点,右部分都是大于privot的节点。
例如:链表9-0-4-5-1,pivot=3。
调整后是1-0-4-9-5,也可以是0-1-9-5-4。

思路:解法一:我们可以利用数组额外空间,利用数组partition排序来实现。时间o(n) 空间o(n)

​1. 先遍历一遍链表,得到链表长度。
​2. 建立一个链表数组。
​3. 利用三向快排的划分
​4. 调整数组中的next值。
​

解法二:遍历链表,拆分三个链表small, equal, big表头,最后再连接即可。时间o(n) 空间o(1)

class Node {
    public int val;
    public Node next;
    public Node(int x) {
        val = x;
        this.next = null;
    }
}
public class Solution2 {
    //解法一:我们可以利用数组额外空间来实现
    //时间o(n) 空间o(n)
    //1. 先遍历一遍链表,得到链表长度。
    //2. 建立一个链表数组。
    //3. 利用三向快排的划分
    //4. 调整数组中的next值。
    public static Node listPartition1(Node head, int privot){
        if(head==null || head.next==null){
            return head;
        }
        //1.遍历链表得到长度。
        int len = 0;
        Node p = head;
        while(p!=null){
            len++;
            p = p.next;
        }
        //2.建立一个链表数组。
        Node[] arr = new Node[len];
        p = head;
        for(int i=0; i<len; ++i){
            arr[i] = p;
            p = p.next;
        }
        //3.数组partition排序
        paitition(arr, 0, len-1, privot);
        //4.重新连接数组的node
        for(int i=0; i<len-1; ++i){
            arr[i].next = arr[i+1];
        }
        arr[len-1].next = null;
        return arr[0];
    }
    public static void paitition(Node[] arr, int left, int right, int privot){
        //注意的这里的partition保持了数组原来的顺序
        int small = left;
        int big = right;
        for(int i=left; i<=right&&i<=big; ++i){
            if(arr[i].val<privot){
                swap(arr, i, small++);
            }
            else if(arr[i].val>privot){
                swap(arr, i, big--);
            }
        }
        while(big<right){
            swap(arr, big++, right--);
        }
    }
	
    //解法二:遍历链表,拆分三个链表small, equal, big表头,最后再连接即可
    //时间o(n) 空间o(1)
    public static Node listPartition2(Node head, int privot){
        if(head==null || head.next==null){
            return head;
        }
        Node small = new Node(0);
        Node smallCopy = small;
        Node equal = new Node(0);
        Node equalCopy = equal;
        Node big = new Node(0);
        Node bigCopy = big;
        Node p = head;
        while(p!=null){//遍历链表
            if(p.val<privot){
                Node temp = p;
                p = p.next;
                temp.next = null;
                small.next = temp;
                small = temp;
            }
            else if(p.val == privot){
                Node temp = p;
                p = p.next;
                temp.next = null;
                equal.next = temp;
                equal = temp;
            }
            else{
                Node temp = p;
                p = p.next;
                temp.next = null;
                big.next = temp;
                big = temp;
            }
        }
        small.next = equalCopy.next;
        equal.next = bigCopy.next;
        return smallCopy.next;
    }
	
    public static void main(String[] args){
        Node head = new Node(9);
        Node p = head;
        p.next = new Node(0);
        p = p.next;
        p.next = new Node(3);
        p = p.next;
        p.next = new Node(4);
        p = p.next;
        p.next = new Node(5);
        p = p.next;
        p.next = new Node(1);
        show(head);
//        head = listPartition1(head, 3);
        head = listPartition2(head, 3);
        System.out.println();
        show(head);
    }
    public static void show(Node head){
        while(head!=null){
            System.out.print(head.val + " ");
            head = head.next;
        }
    }
    public static void swap(Node[] arr, int i, int j){
        Node temp = arr[i];
        arr[i] = arr[j];
        arr[j] = temp;
    }
}
//out: 9 0 3 4 5 1 
	   0 1 3 9 4 5 
	   

14.单链表的归并排序

leetcode148

题目:Sort a linked list in O(n log n) time using constant space complexity.

思路:归并排序应该是单链表排序最好的方法了,时间o(nlogn),空间o(1)。分而治之,先找到中间节点,拆分成两个链表,merge

public static ListNode sortList(ListNode head) {
    if(head==null || head.next==null){
        return head;
    }
    ListNode left = head;
    ListNode right = head;
    while(right.next!=null && right.next.next!=null){
        left = left.next;
        right = right.next.next;
    }

    right = left.next;
    left.next = null;//将链表从中间节点拆分,分离成两个链表
    left = sortList(head);
    right = sortList(right);
    return merge(left, right);
}

public static ListNode merge(ListNode l1, ListNode l2){
    ListNode guard = new ListNode(0);
    ListNode p = guard;
    ListNode temp;
    while(l1!=null && l2!=null){
        if(l1.val<=l2.val){
            temp = l1;
            l1 = l1.next;
        }
        else{
            temp = l2;
            l2 = l2.next;
        }
        temp.next = null;
        p.next = temp;
        p = p.next;
    }
    if(l1!=null){
        p.next = l1;
    }
    if(l2!=null){
        p.next = l2;
    }
    return guard.next;
}
public static void show(ListNode head){
    while(head!=null){
        System.out.print(head.val + " ");
        head = head.next;
    }
    System.out.println();
}
public static void main(String[] args){
    ListNode head = new ListNode(1);
    head.next = new ListNode(2);
    head.next.next = new ListNode(5);
    head.next.next.next = new ListNode(4);
    head.next.next.next.next = new ListNode(7);
    show(head);
    show(sortList(head));
    //out:1 2 5 4 7
    //    1 2 4 5 7
}

15.单链表的快速排序

leetcode148

题目:Sort a linked list in O(n log n) time using constant space complexity.

Input: 4->2->1->3
Output: 1->2->3->4

思路:partition思想,用三个指针来控制,pBase指针指向枢纽值结点,pleft指针指向当前最后一个比枢纽值小的结点,pright结点用于遍历,将遇到的比pBase小的结点的值交换到前面去。

左神刷题_第1张图片

//单链表的快排
public ListNode sortList(ListNode head) {
    if(head==null || head.next==null){
        return head;
    }
    ListNode tail = head;
    while(tail.next!=null){
        tail = tail.next;
    }
    quickSortList(head, tail);
    return head;
}
public void quickSortList(ListNode head, ListNode tail){
    if(head==tail || head==null || tail==null){
        return;
    }
    ListNode pBase = head; //基准key节点
    ListNode pSmall = head; //此节点之前的节点都是比pBase小的
    ListNode cur = head.next; //遍历节点
    while(cur!=tail.next){
        if(cur.val<pBase.val){
            pSmall = pSmall.next;
            swap(pSmall, cur);
        }
        cur = cur.next;
    }
    swap(pBase, pSmall);
    quickSortList(head, pSmall);
    quickSortList(pSmall.next, tail);
}
public void swap(ListNode a, ListNode b){
    int temp = a.val;
    a.val = b.val;
    b.val = temp;
}

16.单链表的选择排序

《左神》79、和88题(单链表快排)一起看

题目:单链表的选择排序

思想:时间o(n^2),空间o(1),正常的选择排序思想,每次都从剩下链表头结点遍历,选出剩下链表中最小的节点,接入到排序好的链表部分,这里注意节点的断开与接入的指针变量操作

//单链表的选择排序。时间o(n^2)
public static ListNode selectSortList(ListNode head) {
    if(head==null || head.next==null){
        return head;
    }
    ListNode resHeadCopy = new ListNode(0); //哨兵,最终返回resHeadCopy.next
    ListNode resHead = resHeadCopy; //存储排序链表的最后一个节点
    while(head.next!=null){//选择排序,每次都从剩下链表头结点遍历,选出剩下链表中最小的节点,连接到resHead后
        ListNode minTemp = head;
        ListNode minPre=null;
        ListNode cur = head.next;//遍历剩下链表
        ListNode curPre = head;
        while(cur!=null){//每次都从剩下链表头结点遍历,找剩下链表中最小节点
            if(cur.val<minTemp.val){
                minTemp = cur;
                minPre = curPre;
            }
            curPre = cur;
            cur = cur.next;
        }
        ListNode temp = minTemp;//这是找到的min节点
        if(temp==head){//如果min节点是头结点
            head = head.next;//断开min节点
        }
        else{
            minPre.next = minPre.next.next; //断开min节点
        }
        temp.next = null;//断开min节点
        resHead.next = temp;//min节点连入排序链表尾
        resHead = temp;
    }
    resHead.next = head; //链表剩下最后一个节点接入
    return resHeadCopy.next;
}

17.两个链表生成相加链表

《左神》59、leetcode 2

题目:You are given two non-empty linked lists representing two non-negative integers. The digits are stored in reverse order and each of their nodes contain a single digit. Add the two numbers and return it as a linked list.

You may assume the two numbers do not contain any leading zero, except the number 0 itself.

Input: (2 -> 4 -> 3) + (5 -> 6 -> 4)
Output: 7 -> 0 -> 8
Explanation: 342 + 465 = 807.

思路:遍历两个链表,每个节点相加放进新建链表即可,有个cnt变量记录进位,注意如果最后还有进位,别忘了再生成一个节点

//两个链表生成相加链表。思路:遍历两个链表,每个节点相加放进新建链表即可,有个cnt变量记录进位
public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
    ListNode guard = new ListNode(0);
    ListNode p = guard;
    int cnt = 0;//进位
    int sum;//每两个节点的和
    while(l1!=null && l2!=null){
        sum = l1.val + l2.val + cnt;
        cnt = sum/10;
        p.next = new ListNode(sum%10);
        p = p.next;
        l1 = l1.next;
        l2 = l2.next;
    }
    while(l1!=null){
        sum = l1.val + cnt;
        cnt = sum/10;
        p.next = new ListNode(sum%10);
        p = p.next;
        l1 = l1.next;
    }
    while(l2!=null){
        sum = l2.val + cnt;
        cnt = sum/10;
        p.next = new ListNode(sum%10);
        p = p.next;
        l2 = l2.next;
    }
    if(cnt!=0){//如果还有进位,则再创建一个节点
        p.next = new ListNode(1);
    }
    return guard.next;
}

18.删除单链表的重复节点(三道)

1.链表有序,保留一个

leetcode 83

题目:Given a sorted linked list, delete all duplicates such that each element appear only once.

Input: 1->1->2->3->3
Output: 1->2->3

思路:时间o(n),空间o(1),遍历删除即可

public static ListNode deleteDuplicates(ListNode head) {
    if(head==null || head.next==null){
        return head;
    }
    ListNode p = head;
    ListNode cur = head.next;
    while(cur!=null){
        if(p.val==cur.val){
            p.next = cur.next;
        }
        else{
            p = cur;
        }
        cur = cur.next;
    }
    return head;
}

2.链表有序,不保留

leetcode 82

题目:Given a sorted linked list, delete all nodes that have duplicate numbers, leaving only distinct numbers from the original list.

Example 1:

Input: 1->2->3->3->4->4->5
Output: 1->2->5

思路:定义一个哨兵,定义一个前驱指针和一个现指针,每当前驱指针指向新建的节点,现指针从下一个位置开始往下遍历,遇到相同的则继续往下,直到遇到不同项时,把前驱指针的next指向下面那个不同的元素。如果现指针遍历的第一个元素就不相同,则把前驱指针向下移一位。

代码:

public ListNode deleteDuplicates(ListNode head) {
    if(head==null || head.next==null){
        return head;
    }
    
    ListNode guard = new ListNode(0);//哨兵
    guard.next = head;
    
    ListNode pre = guard;//前驱指针
    ListNode cur = guard.next;
    
    while(cur!=null){
        if(cur.next!=null && cur.next.val==cur.val){//有重复,要准备删除了
            int temp = cur.val;//缓存要删除的val
            while(cur!=null && cur.val==temp){
                cur = cur.next;
            }
            pre.next = cur;
        }
        else{
            pre = cur;
            cur = cur.next;
        }
    }
    
    return guard.next;
}

3.链表无序

《左神》71

题目:Given a sorted linked list, delete all duplicates such that each element appear only once.

Input: 1->2->2->1->3
Output: 1->2->3

思路:哈希set。时间o(n),空间o(n)

public static void deleteDuplicate(ListNode head){
    if(head==null || head.next==null){
        return;
    }
    HashSet<Integer> set = new HashSet<>();//哈希表
    ListNode p = head;
    ListNode cur = head.next;
    set.add(p.val);
    while(cur!=null){
        if(set.contains(cur.val)){
            p.next = cur.next;
        }
        else{
            set.add(cur.val);
            p = cur;
        }
        cur = cur.next;
    }
}

19.向有序的环形单链表中插入新节点

《左神》82

题目:一个环形单链表从头节点 head 开始不降序,同时由最后的节点指回头节点。给定这样的一个环形单链表的头节点 head 和 一个整数 num ,请生成节点值为 num 的新节点,并插入到这个环形链表中,保证调整后的链表依然有序。

思路:直接从头结点遍历插入即可,时间复杂度为 O(N)、额外空间复杂度为 O(1)。

//环形有序链表中插入新节点
public static ListNode insertNum(ListNode head, int num){
    ListNode newNode = new ListNode(num);
    if(head==null){
        newNode.next = newNode;
        return newNode;
    }
    ListNode cur = head;
    if(num <= head.val){//如果要插入到头结点之前
        while(cur.next!=head){
            cur = cur.next;
        }
        newNode.next = head;
        cur.next = newNode;
        head = newNode;
    }
    else{
        while(true){
            if(cur.next.val>=num){
                newNode.next = cur.next;
                cur.next = newNode;
                break;
            }
            cur = cur.next;
        }
    }
    return head;
}

20.打印二叉树的边界节点

《左神》95、 leetcode545(会员)

题目:给定一颗二叉树的头结点head,按照如下两种标准分别实现二叉树边界节点的逆时针打印。

1.头节点为边界节点
2.叶结点为边界节点
3.如果节点在其所在的层中是最左边或最右边,那么也是边界节点

21.编辑距离

leetcode 72

题目:Given two words word1 and word2, find the minimum number of operations required to convert word1 to word2.You have the following 3 operations permitted on a word:

Insert a character
Delete a character
Replace a characte

Example 1:

Input: word1 = "horse", word2 = "ros"
Output: 3
Explanation: 
horse -> rorse (replace 'h' with 'r')
rorse -> rose (remove 'r')
rose -> ros (remove 'e')
	
Input: word1 = "intention", word2 = "execution"
Output: 5
Explanation: 
intention -> inention (remove 't')
inention -> enention (replace 'i' with 'e')
enention -> exention (replace 'n' with 'x')
exention -> exection (replace 'n' with 'c')
exection -> execution (insert 'u')

思路:DP问题。维护一个二维的数组dp,其中dp[i][j]表示从word1的前i个字符转换到word2的前j个字符所需要的步骤。那我们可以先给这个二维数组dp的第一行第一列赋值,因为第一行和第一列对应的总有一个字符串是空串,于是转换步骤完全是另一个字符串的长度。

转移方程:当word1[i]==word2[j]时,dp[i][j] = dp[i-1][j-1],其他情况时,dp[i][j]是其左,左上,上的三个值中的最小值加1。

if word1[i-1]==word2[j-1]:
	dp[i][j] = dp[i - 1][j - 1]
else: 
	dp[i][j] = min(dp[i-1][j-1], dp[i-1][j], dp[i][j-1]) + 1 //替换、删除、插入   
	     

代码:

public int minDistance(String word1, String word2) {
    if(word1==null || word2==null){
        return 0;
    }
    int len1 = word1.length();
    int len2 = word2.length();

    int[][] dp = new int[len1+1][len2+1];
    for (int i=0; i<=len2; ++i){//第一行
        dp[0][i] = i;
    }
    for (int i=0; i<=len1; ++i){//第一列
        dp[i][0] = i;
    }

    for(int i=1; i<=len1; ++i){
        for (int j=1; j<=len2; ++j){
            if(word1.charAt(i-1) == word2.charAt(j-1)){
                dp[i][j] = dp[i-1][j-1];
            }
            else{
                dp[i][j] = min(dp[i-1][j-1], dp[i][j-1], dp[i-1][j])+1;
            }
        }
    }
    return dp[len1][len2];
}

public int min(int a, int b, int c){
    int temp = Math.min(a,b);
    return Math.min(temp,c);
}

22.找出数组中左边比他小右边比他大的所有元素

题目:给定一个不重复的数组,找出所有符合条件的元素:该元素左边都比它小,右边都比它大。e.g. [1,2,3,4,5]–>out:[1,2,3,4,5]所有元素都满足。时间要求o(n)

思路:辅助一个o(n)数组temp[],遍历两次数组即可。第一次从后往前记录temp[],temp[i]表示从结尾到i时最小的数,第二次遍历从前往后,tempMax存从头到i时最大的数,如果nums[i]比tempMax大且比temp[i]小则nums[i]符合条件。

//找出数组中左边比他小右边比他大的所有元素。思路:辅助o(n)的temp[],遍历两遍数组即可
public static ArrayList<Integer> findEleBigLeftSmallRight(int[] numbers){
    ArrayList<Integer> res = new ArrayList<>();
    if(numbers==null || numbers.length==0){
        return res;
    }
    int len = numbers.length;
    if(len==1){//只有一个节点时直接返回该节点,无需比较
        res.add(numbers[0]);
        return res;
    }

    //1.从后往前遍历,存最小的数
    int[] temp = new int[len];//辅助数组
    temp[len-1] = numbers[len-1];
    for(int i=len-2; i>=0; --i){
        temp[i] = numbers[i]<temp[i+1] ? numbers[i] : temp[i+1];
    }

    //2.从前往后遍历,找符合条件的数
    int tempMax = Integer.MIN_VALUE;//存最大的数
    for(int i=0; i<len-1; ++i){
        if(numbers[i]>tempMax && numbers[i]<temp[i+1]){
            res.add(numbers[i]);//符合条件
        }
        if(numbers[i]>tempMax){
            tempMax = numbers[i];
        }
    }
    if(numbers[len-1]>tempMax){//最后一个数
        res.add(numbers[len-1]);
    }
    return res;
}

23.判断二叉树为BST(二叉搜索树)

leetcode 98

题目:判断二叉树为BST(二叉搜索树)

思路:中序遍历判断且使用全局变量记录前继节点的值

代码:

private Long last = Long.MIN_VALUE;//全局变量记录前继节点的值
//中序遍历判断
public boolean isValidBST(TreeNode root) {
    if(root==null){
        return true;
    }    
    if(!isValidBST(root.left)){
        return false;
    }
    if(root.val<=last){
        return false;
    }
    last = Long.valueOf(root.val);
    return isValidBST(root.right);
}

24.Replace Words(字典树应用)

leetcode 648

题目:In English, we have a concept called root, which can be followed by some other words to form another longer word - let’s call this word successor. For example, the root an, followed by other, which can form another word another.

Now, given a dictionary consisting of many roots and a sentence. You need to replace all the successor in the sentence with the root forming it. If a successor has many roots can form it, replace it with the root with the shortest length.

You need to output the sentence after the replacement.

Example 1:

Input: dict = ["cat", "bat", "rat"]
sentence = "the cattle was rattled by the battery"
Output: "the cat was rat by the bat"

Note:
The input will only have lower-case letters.
1 <= dict words number <= 1000
1 <= sentence words number <= 1000
1 <= root length <= 100
1 <= sentence words length <= 1000

题目大意:给定一个字典,里面是词根root,给定一句话,如果这句话的单词前缀是字典中词根,则将单词用词根替换,如果有多个词根,找最短的词根替换,最后返回替换好的话

思路1(不可取):暴力替换。将这句话split成words[]单词组,将每个单词从前往后字符组的依次与hash字典中的词根比较,如果出现就替换。时间o(nk),n这句话,k字典,空间辅助hash o(k)。时间效率很低

public String replaceWords(List<String> dict, String sentence) {
    HashSet<String> set = new HashSet<>(dict);
    String[] words = sentence.split(" ");
    StringBuilder sb = new StringBuilder();
    for(int i=0; i<words.length; ++i){
        for(int j=1; j<words[i].length(); ++j){
            String root = words[i].substring(0,j);
            if(set.contains(root)){
                words[i] = root;
                break;
            }
        }
        sb.append(words[i] + " ");
    }
    return sb.toString().trim();
}

思路2(可取,但还是不够好):不暴力替换,采用了字典树的思想。1.字典的roots词根用首字母索引,首字母相同的前缀都放到同一个HashSet中,总共需要26个索引数组;2.将这句话split成words[]单词组,遍历words单词数组,一个单词一个单词的在字典树中替换,最后返回替换好的话。时间o(n),空间o(k),n是原句子,k是字典

//思路:字典树思想。将字典中的词根按照首字母构建字典
public String replaceWords(List<String> dict, String sentence) {

    HashSet[] set = new HashSet[26];//字典树,用首字母索引,首字母相同的前缀都放到同一个HashSet中,总共需要26个索引数组
    String[] words = sentence.split(" ");
    StringBuilder sb = new StringBuilder();

    //1.构建字典树,26个字母开头索引
    for(int i=0; i<dict.size(); ++i){
        String word = dict.get(i);
        int wordIndex =  word.charAt(0)-'a';
        if(set[wordIndex]==null){
            set[wordIndex] = new HashSet<String>();
        }
        set[wordIndex].add(word);
    }

	//2.遍历words单词数组,一个单词一个单词的在字典树中替换
    for(int i=0; i<words.length; ++i){
        int wordIndex = words[i].charAt(0)-'a';
        if(set[wordIndex]==null){//这个单词在字典树中没有首字母索引,即也没有词根对应,continue
            sb.append(words[i] + " ");
            continue;
        }
        Iterator<String> iterator = set[wordIndex].iterator();
        while(iterator.hasNext()) {
            String root = iterator.next();
            if (words[i].startsWith(root) && root.length() < words[i].length()) {//找最短的词根替换
                words[i] = root;
            }
        }
        sb.append(words[i] + " ");//替换好的单词加入到新话中
    }

    return sb.toString().trim();
}

思路3(高效字典树,棒):思路2只是很简单用首字母进行索引实现字典树,但还是不够,应该使用字典树(也叫前缀树)实现高效查询。

//字典树节点
class TrieNode{
    char c;
    TrieNode[] children = new TrieNode[26];
    boolean isComplete = false;
    public TrieNode(char c){
        this.c = c;
    }
}

class Solution {
    //思路:构建字典树。
    public String replaceWords(List<String> dict, String sentence) {
        if(sentence==null || sentence.length()<2 || dict==null || dict.size()==0){
            return sentence;
        }
        
        //1.遍历字典中的词根,构建字典树
        TrieNode root = new TrieNode('c');//字典树的根节点
        for(String dictWord: dict){//遍历字典中的词根
            char[] chs = dictWord.toCharArray();
            TrieNode temp = root;
            for(char c: chs){//对每个词根进行字典树构建
                if(temp.children[c-'a']==null){
                    temp.children[c-'a'] = new TrieNode(c);
                }
                temp = temp.children[c-'a'];
            }
            temp.isComplete = true;//一个词根构建完,标记位置为true
        }
        
        //2.遍历原话的words[]单词数组,对每个单词进行字典树替换
        StringBuilder sb = new StringBuilder();
        String[] words = sentence.split(" ");
        for(int i=0; i<words.length; ++i){//遍历原单词数组words[]
            TrieNode temp = root;
            StringBuilder tempSb = new StringBuilder();
            boolean can_change = false;//是否找到一个可以替换的词根
            for(char ch: words[i].toCharArray()){
                if(temp.children[ch-'a']==null){//当前字符在字典树中没有索引
                    break;
                }
                tempSb.append(ch);
                temp = temp.children[ch-'a'];
                if(temp.isComplete==true){//找到了当前单词对应的一个词根
                    can_change = true;
                    break;
                }
            }
            if(can_change){//将单词替换成词根
                sb.append(tempSb.toString() + " ");
            }
            else{//没有对应的词根,单词不变
                sb.append(words[i] + " ");
            }
        }
        
        return sb.toString().trim();
    }
}

25.BFS/DFS思想

参考:LeoYang
Coding and learning
BFS和DFS详解以及java实现

总的来说,BFS多用于寻找最短路径的问题,DFS多用于快速发现底部节点。

BFS广度优先搜索/遍历

BFS主要思想是从起始点开始,将其邻近的所有顶点都加到一个队列中去,然后标记下这些顶点离起始顶点的距离为1.最后将起始顶点标记为已访问,今后就不会再访问。然后再从队列中取出最先进队的顶点A,也取出其周边邻近节点,加入队列末尾,将这些顶点的距离相对A再加1,最后离开这个顶点A。依次下去,直到队列为空为止。从上面描述的过程我们知道每个顶点被访问的次数最多一次(已访问的节点不会再访问),而对于连通图来说,每个顶点都会被访问。加上每个顶点的邻接链表都会被遍历,因此BFS的时间复杂度是O(V+E),其中V是顶点个数,E是边数,也就是所有邻接表中的元素个数。

DFS深度优先搜索/遍历

DFS深度优先搜索是从起始顶点开始,递归访问其所有邻近节点,比如A节点是其第一个邻近节点,而B节点又是A的一个邻近节点,则DFS访问A节点后再访问B节点,如果B节点有未访问的邻近节点的话将继续访问其邻近节点,否则继续访问A的未访问邻近节点,当所有从A节点出去的路径都访问完之后,继续递归访问除A以外未被访问的邻近节点。

26.BFS相关题目(1道)

BFS相关题目_1.01 Matrix

leetcode 542

题目:Given a matrix consists of 0 and 1, find the distance of the nearest 0 for each cell. The distance between two adjacent cells is 1.

Example 1: 
Input:
0 0 0
0 1 0
0 0 0

Output:
0 0 0
0 1 0
0 0 0

Example 2: 
Input:
0 0 0
0 1 0
1 1 1

Output:
0 0 0
0 1 0
1 2 1

Note:
The number of elements of the given matrix will not exceed 10,000.
There are at least one 0 in the given matrix.
The cells are adjacent in only four directions: up, down, left and right.

思路:BFS。这道题给了我们一个只有0和1的矩阵,让我们求每一个1到离其最近的0的距离,其实也就是求一个距离场,而求距离场那么BFS将是不二之选。

1.首先遍历一次矩阵,将值为0的点都存入queue,将值为1的点换成MAX_VALUE;
2.对队列中的每一个0进行BFS上下左右搜索:从queue中取出一个数字,遍历其周围四个点,如果越界或者周围点的值小于等于当前值加1,则直接跳过;否则将周围点的值更新为当前值加1,然后把周围点的坐标加入queue

代码:

public int[][] updateMatrix(int[][] matrix) {
    if(matrix==null || matrix.length==0 || matrix[0].length==0){
        return matrix;
    }
    int m = matrix.length;
    int n = matrix[0].length;

    //1.把原矩阵中1换成MAX_VALUE; 2.把0全部放进队列
    Queue<Pair<Integer,Integer> > queue = new LinkedList<>();
    for(int i=0; i<m; ++i){
        for (int j=0; j<n; ++j){
            if(matrix[i][j]==0){
                queue.offer(new Pair<>(i, j));
            }
            else{
                matrix[i][j] = Integer.MAX_VALUE;
            }
        }
    }

    //3.对队列中的每一个0进行BFS上下左右搜索
    int[][] dirs = new int[][]{{1,0}, {-1,0}, {0,-1}, {0,1}}; //上下左右
    while(!queue.isEmpty()){
        Pair<Integer, Integer> origin = queue.poll();
        for(int i=0; i<4; ++i){
            int x = origin.getKey() + dirs[i][0];
            int y = origin.getValue() + dirs[i][1];
            if(x<0 || x>=m || y<0 || y>=n || matrix[x][y]<=( matrix[origin.getKey()][origin.getValue()]+1 ) ){
                continue;
            }
            matrix[x][y] = matrix[origin.getKey()][origin.getValue()]+1;

            queue.offer(new Pair<>(x, y));//BFS入队别忘了
        }
    }
    return matrix;
}

27.DFS相关题目(4道)

DFS相关题目_1.Number of Islands

leetcode 200

题目:Given a 2d grid map of '1’s (land) and '0’s (water), count the number of islands. An island is surrounded by water and is formed by connecting adjacent lands horizontally or vertically. You may assume all four edges of the grid are all surrounded by water.

Example 1:

Input:
11110
11010
11000
00000

Output: 1

思路:找岛屿的个数。思路:dfs,找到图中值为1的点,分别从上、下、左、右四个方向搜索。搜索过的‘1’就置为‘0’,每完成一整片陆地的搜索,计数器加1

代码:

public int numIslands(char[][] grid) {
    if(grid==null || grid.length==0 || grid[0].length==0){
        return 0;
    }
    int res = 0;
    int m = grid.length;
    int n = grid[0].length;
    for(int i=0; i<m; ++i){
        for(int j=0; j<n; ++j){
            if(grid[i][j]=='1'){//找到一个岛屿
                dfs(grid, m, n, i, j);//对该岛屿进行dfs搜索土地,并将岛屿的全部土地变为'0'已访问
                res++;
            }
        }
    }
    return res;
}
public void dfs(char[][] grid, int m, int n, int i, int j){
    if(i<0 || i>=m || j<0 || j>=n || grid[i][j]=='0'){
        return;
    }
    grid[i][j] = '0';//标记为已访问
    dfs(grid, m , n, i-1, j);
    dfs(grid, m , n, i+1, j);
    dfs(grid, m , n, i, j-1);
    dfs(grid, m , n, i, j+1);//上下左右
}

DFS相关题目_2.Max Area of Island

leetcode 695

题目:Given a non-empty 2D array grid of 0’s and 1’s, an island is a group of 1’s (representing land) connected 4-directionally (horizontal or vertical.) You may assume all four edges of the grid are surrounded by water.

Find the maximum area of an island in the given 2D array. (If there is no island, the maximum area is 0.)

Example 1:

[[0,0,1,0,0,0,0,1,0,0,0,0,0],
 [0,0,0,0,0,0,0,1,1,1,0,0,0],
 [0,1,1,0,1,0,0,0,0,0,0,0,0],
 [0,1,0,0,1,1,0,0,1,0,1,0,0],
 [0,1,0,0,1,1,0,0,1,1,1,0,0],
 [0,0,0,0,0,0,0,0,0,0,1,0,0],
 [0,0,0,0,0,0,0,1,1,1,0,0,0],
 [0,0,0,0,0,0,0,1,1,0,0,0,0]]
 
 Given the above grid, return 6. Note the answer is not 11, because the island must be connected 4-directionally.(斜着的不算)

思路:在很多岛屿中,返回最大岛屿面积。思路:dfs,找到图中值为1的点,分别从上、下、左、右四个方向搜索。搜索过的‘1’就置为‘0’,每完成一整片陆地的搜索并计算面积。

代码:

public int maxAreaOfIsland(int[][] grid) {
    if(grid==null || grid.length==0 || grid[0].length==0){
        return 0;
    }
    int resMax = 0;
    int m = grid.length;
    int n = grid[0].length;
    for(int i=0; i<m; ++i){
        for(int j=0; j<n; ++j){
            if(grid[i][j]==1){//找到一个岛屿
                int[] tempMax = new int[]{0};//计算当前岛屿的面积
                dfs(grid, m, n, i, j, tempMax);//对该岛屿进行dfs搜索土地并计算面积,并将岛屿的全部土地变为'0'已访问
                resMax = Math.max(resMax, tempMax[0]);
            }
        }
    }
    return resMax;
}

public void dfs(int[][] grid, int m, int n, int i, int j, int[] tempMax){
    if(i<0 || i>=m || j<0 || j>=n || grid[i][j]==0){
        return;
    }
    grid[i][j] = 0;//标记为已访问
    tempMax[0]++;
    dfs(grid, m , n, i-1, j, tempMax);
    dfs(grid, m , n, i+1, j, tempMax);
    dfs(grid, m , n, i, j-1, tempMax);
    dfs(grid, m , n, i, j+1, tempMax);//上下左右
}

DFS相关题目_3.Island Perimeter

leetcode 463

题目大意:二维地图,每个单元格的长度为1的方形。1代表陆地,0代表水,上、下、左、右四个方向的单元格相连,求出相连陆地单元格的周长。

思路:图中只有唯一一个岛屿,不用dfs,直接遍历图中所有的节点,对是岛屿的每一个节点计算其贡献的周长,加起来即可。

代码:

//求图中唯一岛屿的周长。思路://遍历图中所有的节点,对是岛屿的每一个节点计算其贡献的周长,加起来即可
//对岛屿的一个节点计算周长的规律:周围相邻有0/1/2/3/4的节点的话,对应的周长分别为4/3/3/2/1/0
public int islandPerimeter(int[][] grid) {
    if(grid==null || grid.length==0 || grid[0].length==0){
        return 0;
    }
    int resPrm = 0;//该岛屿的周长
    int m = grid.length;
    int n = grid[0].length;
    for(int i=0; i<m; ++i){//遍历图中所有的节点,对是岛屿的每一个节点计算其贡献的周长,加起来即可
        for(int j=0; j<n; ++j){
            if(grid[i][j]==1){
                int temp = 4;
                if(i-1>=0 && grid[i-1][j]==1){//上:有相邻的一个节点
                    temp--;
                }
                if(i+1<m && grid[i+1][j]==1){//下:有相邻的一个节点
                    temp--;
                }
                if(j-1>=0 && grid[i][j-1]==1){//左:有相邻的一个节点
                    temp--;
                }
                if(j+1<n && grid[i][j+1]==1){//右:有相邻的一个节点
                    temp--;
                }
                resPrm += temp;
            }
        }
    }
    return resPrm;
}

DFS相关题目_4.Surrounded Regions

leetcode 130

题目:这道题的意思是将所有被X包围的O都变为X(边缘的不算)

思路:1.从图的4个边界出发DFS找O的土地,全部换成*;2.遍历图中剩下的节点,将O换成X,将*换成O

代码:

public void solve(char[][] board) {
    if(board==null || board.length==0 || board[0].length==0){
        return;
    }
    int m = board.length;
    int n = board[0].length;
    //1.从图的4个边界出发DFS找O的土地,全部换成*
    for (int i = 0; i < n; ++i){
        dfsSolve(board, m, n, 0, i);//上边界
        dfsSolve(board, m, n, m-1, i);//下边界
    }
    for (int i = 0; i < m; ++i){
        dfsSolve(board, m, n, i, 0);//左边界
        dfsSolve(board, m, n, i, n-1);//右边界
    }

    //2.遍历图中剩下的节点,将O换成X,将*换成O
    for (int i = 0; i < m; ++i) {
        for (int j = 0; j < n; ++j) {
            if(board[i][j]=='*'){
                board[i][j]='O';
            }
            else if(board[i][j]=='O'){
                board[i][j]='X';
            }
        }
    }
}
//从图的4个边界出发DFS找O的土地,访问过的换成*
public void dfsSolve(char[][] grid, int m, int n, int i, int j){
    if(i<0 || i>=m || j<0 || j>=n || grid[i][j]=='X' || grid[i][j]=='*'){
        return;
    }
    grid[i][j] = '*';//把边界O换成*
    dfsSolve(grid, m, n, i+1, j);//上
    dfsSolve(grid, m, n, i-1, j);//下
    dfsSolve(grid, m, n, i, j-1);//左
    dfsSolve(grid, m, n, i, j+1);//右
}

28.图的几种最短路算法(4道)

1.单源最短路径_1.Dijkstra算法
2.单源最短路径_2.Bellman-Ford算法
3.单源最短路径_3.SPFA算法
4.多源最短路径_Floyd-Warshall算法

1.单源最短路径_1.Dijkstra算法

普通实现的时间复杂度为O(V2),若基于 Fibonacci heap 的最小优先队列实现版本则时间复杂度为 O(E+VlogV)

Dijkstra算法采用的是一种贪心的策略,声明一个数组dis来保存源点到各个顶点的最短距离和一个保存已经找到了最短路径的顶点的集合:T,初始时,原点 s 的路径权重被赋为 0 (dis[s] = 0)。若对于顶点 s 存在能直接到达的边(s,m),则把dis[m]设为w(s, m),同时把所有其他(s不能直接到达的)顶点的路径长度设为无穷大。初始时,集合T只有顶点s

然后,从dis数组选择最小值,则该值就是源点s到该值对应的顶点的最短路径,并且把该点加入到T中,此时完成一个顶点, 然后,我们需要看看新加入的顶点是否可以到达其他顶点并且看看通过该顶点到达其他点的路径长度是否比源点直接到达短,如果是,那么就替换这些顶点在dis中的值。 然后,又从dis中找出最小值,重复上述动作,直到T中包含了图的所有顶点。

图片截图:

左神刷题_第2张图片

自己跑过的代码:

/**
 * @FileName: Dijkstra
 * @Author: braincao
 * @Date: 2019/1/9 16:55
 * @Description: Dijkstra最短路径算法实现
 */
public class Dijkstra {
    private static int N = Integer.MAX_VALUE - 2;
    private static int[][] Graph = {
            {0, 1, 5, N, N, N, N, N, N},
            {1, 0, 3, 7, 5, N, N, N, N},
            {5, 3, 0, N, 1, 7, N, N, N},
            {N, 7, N, 0, 2, N, 3, N, N},
            {N, 5, 1, 2, 0, 3, 6, 9, N},
            {N, N, 7, N, 3, 0, N, 5, N},
            {N, N, N, 3, 6, N, 0, 2, 7},
            {N, N, N, N, 9, 5, 2, 0, 4},
            {N, N, N, N, N, N, 7, 4, 0}};

    public static void main(String[] args) {
        dijkstra(0, Graph);
    }

    /**
     * Dijkstra最短路径。
     * 即图中"节点vs"到其它各个节点的最短路径。
     *
     * @param vs    起始节点
     * @param Graph 图
     */
    public static void dijkstra(int vs, int[][] Graph) {

        int NUM = Graph.length;//图中节点的个数

        int[] prenode = new int[NUM];//前驱节点数组

        int[] mindist = new int[NUM];// 最短距离数组

        boolean[] find = new boolean[NUM];// 该节点是否已经找到最短路径,即已经确定节点的集合,初始里面只有vs节点

        int vnear = 0;

        for (int i = 0; i < NUM; i++) {//初始化
            prenode[i] = i;
            mindist[i] = Graph[vs][i];
            find[i] = false;
        }

        find[vs] = true;//节点自己到自己的最短路径能找到

        for (int v = 1; v < NUM; v++) {//循环NUM-1次

            // 每次循环找一个距离vs最近的节点vnear和最短距离min
            int min = Integer.MAX_VALUE;
            for (int j = 0; j < NUM; j++) {
                if (!find[j] && mindist[j] < min) {
                    min = mindist[j];
                    vnear = j;
                }
            }
            find[vnear] = true;//vnear节点已经确定,访问标记

            // 根据vnear修正vs到其他所有节点的前驱节点及距离,即松弛操作
            for (int k = 0; k < NUM; k++) {
                if (!find[k] && (min + Graph[vnear][k]) < mindist[k]) {
                    prenode[k] = vnear;
                    mindist[k] = min + Graph[vnear][k];
                }
            }
        }

        for (int i = 0; i < NUM; i++) {
            System.out.println("v" + vs + "...v" + prenode[i] + "->v" + i + ", s=" + mindist[i]);
        }
    }
}

out:

v0...v0->v0, s=0
v0...v1->v1, s=1
v0...v1->v2, s=4
v0...v2->v3, s=-2147483647
v0...v3->v4, s=-2147483645
v0...v7->v5, s=-2147483642
v0...v2->v6, s=-2147483647
v0...v2->v7, s=-2147483647
v0...v2->v8, s=-2147483647

2.单源最短路径_2.Bellman-Ford算法

时间o(VE),比dijkstra慢

Bellman-Ford 算法描述:

1.创建源顶点 v 到图中所有顶点的距离的集合 distSet,为图中的所有顶点指定一个距离值,初始均为 Infinite,源顶点距离为 0;
计算最短路径,执行 V - 1 次遍历,每次遍历中,依赖所有边进行松弛操作;

2.松弛操作:对于图中的每条边:如果起点 u 的距离 d 加上边的权值 w 小于终点 v 的距离 d,则更新终点 v 的距离值 d;

3.检测图中是否有负权边形成了环,遍历图中的所有边,计算 u 至 v 的距离,如果对于 v 存在更小的距离,则说明存在环;

自己跑过的代码:

import java.util.HashSet;
import java.util.Set;
/**
 * @FileName: Dijkstra
 * @Author: braincao
 * @Date: 2019/1/9 16:55
 * @Description: Bellman-Ford最短路径算法实现
 */

class Edge
{
    int start; //有向边的起点
    int end;   //有向边的终点
    int weight;//边的权重
}
public class Bellman {

    private final static int N = 9999;//不可达边的权值

    /**
     * @param: 图结构、源节点的下标(0~节点个数-1)
     * @return
     */
    public void Bellman_Ford(int[][] graph, int source){

        //图结构中的节点数目、边集合
        int nodenum = graph.length;
        Set<Edge> edge = new HashSet<>();//边集合

        //1.根据graph的图结构给edge[]边数组赋值
        for(int i=0; i<nodenum; ++i){
            for(int j=i+1; j<nodenum; ++j){
                if(graph[i][j]!=N && graph[i][j]!=0){//有边的话就赋值,最终有edgeIndex条边
                    Edge tempEdge = new Edge();
                    tempEdge.start = i;
                    tempEdge.end = j;
                    tempEdge.weight = graph[i][j];
                    edge.add(tempEdge);
                }
            }
        }

        //2.为dist最短路径数组初始化赋值
        int[] dist = new int[nodenum];
        for(int i=0; i<nodenum; i++){
            dist[i]=graph[source][i];
        }
        dist[source]=0;

        //3.循环nodenum-1次,每次都遍历所有边,进行松弛操作
        for(int i=0; i<nodenum-1; i++)//循环nodenum-1次
        {
            for (Edge edgeTemp: edge){//每次都遍历所有边,进行松弛操作
                int start = edgeTemp.start;
                int end = edgeTemp.end;
                int weight = edgeTemp.weight;
                if(dist[end]>dist[start]+weight){//松弛操作
                    dist[end]=dist[start]+weight;
                }
            }
        }

        //4.判断是否存在负回路
        boolean flag=false;//是否存在回路
        for (Edge edgeTemp: edge){
            if( dist[edgeTemp.end] > dist[edgeTemp.start]+edgeTemp.weight )
            {
                flag=true;
                break;
            }
        }

        if(!flag){//不存在负回路的话打印dist最短路径
            for(int i=0;i<nodenum;i++)
                System.out.println(dist[i]);//打印源节点到每个节点的距离
        }
    }

    public static void main(String[] args){
        Bellman bellman = new Bellman();
        int[][] graph = new int[][]{
                                    {0, 1, 5, N},
                                    {1, 0, 3, 7},
                                    {5, 3, 0, N},
                                    {N, 7, N, 0}};
        bellman.Bellman_Ford(graph, 2);//节点0、1、2、3,现求节点2的单源最短路径
    }
}

3.单源最短路径_3.SPFA算法

SPFA(Shortest Path Faster Algorithm)算法是求单源最短路径的一种算法,它是Bellman-ford的队列优化,它是一种十分高效的最短路算法。

很多时候,给定的图存在负权边,这时类似Dijkstra等算法便没有了用武之地,而Bellman-Ford算法的复杂度又过高,SPFA算法便派上用场了。SPFA的复杂度大约是O(kE),k是每个点的平均进队次数(一般的,k是一个常数,在稀疏图中小于2)。

但是,SPFA算法稳定性较差,在稠密图中SPFA算法时间复杂度会退化。

实现方法:建立一个队列,初始时队列里只有起始点,在建立一个表格记录起始点到所有点的最短路径(该表格的初始值要赋为极大值,该点到他本身的路径赋为0)。然后执行松弛操作,用队列里有的点去刷新起始点到所有点的最短路,如果刷新成功且被刷新点不在队列中则把该点加入到队列最后。重复执行直到队列为空。

此外,SPFA算法还可以判断图中是否有负权环,即一个点入队次数超过N。

图结构为邻接矩阵的spfa代码:

import java.util.*;
/**
 * @FileName: SPFA
 * @Author: braincao
 * @Date: 2019/1/9 16:55
 * @Description: SPFA最短路径算法实现
 */
public class SPFA {

    private final static int N = 9999;//不可达边的权值

    /**
     * @param: 图结构(邻接矩阵)、源节点的下标(0~节点个数-1)
     * @return
     */
    public static int[] spfa(int[][] graph, int source){

        int nodenum = graph.length;
        int[] dist = new int[nodenum];//最短路径数组
        boolean[] used = new boolean[nodenum];//访问标记位
        for(int i = 0; i<nodenum; i++){
            dist[i] = Integer.MAX_VALUE;
            used[i] = false;
        }
        int[] num = new int[nodenum]; //记录每个节点遍历过的次数,用于检测负向环
        Queue<Integer> queue = new LinkedList<>();//辅助队列
        int path[] = new int[nodenum];         //记录最短路的路径

        dist[source] = 0;     //source顶点到自身距离为0
        used[source] = true;    //表示source顶点进入数组队
        num[source] = 1;       //表示source顶点已被遍历一次
        queue.add(source);      //source顶点入队
        boolean flag = false;//是否存在回路

        while(!queue.isEmpty()) {
            int u = queue.poll();   //获取队头
            used[u] = false;
            for (int v = 0; v < nodenum; ++v) {//对每一个u的相邻节点进行松弛操作
                if (graph[u][v] != N) {u与v直接邻接
                    if (dist[v] > dist[u] + graph[u][v]) {
                        dist[v] = dist[u] + graph[u][v];
                        path[v] = u;
                        if (!used[v]) {//如果v没有访问过,入队
                            queue.offer(v);
                            used[v] = true;
                            num[v]++;
                            if (num[v] >= nodenum) {//遍历次数大于等于节点数,存在负向环,break
                                flag = true;
                                break;
                            }
                        }
                    }
                }
            }
            if(flag){//存在负向环,break
                break;
            }
        }

        if(!flag){//不存在负回路的话打印dist最短路径
            for(int i=0;i<nodenum;i++)
                System.out.println(dist[i]);//打印源节点到每个节点的距离
        }
        return dist;
    }

    public static void main(String[] args){
        int[][] graph = new int[][]{
                                    {0, 1, 5, N},
                                    {1, 0, 3, 7},
                                    {5, 3, 0, N},
                                    {N, 7, N, 0}};
        int[] dist = spfa(graph, 2);//节点0、1、2、3,现求节点2的单源最短路径
        System.out.println(Arrays.toString(dist));//out:[4, 3, 0, 10]
    }
}

图结构为邻接链表的spfa代码:

import java.util.*;
/**
 * @FileName: SPFA
 * @Author: braincao
 * @Date: 2019/1/9 16:55
 * @Description: SPFA最短路径算法实现
 */
public class SPFA {

    private final static int N = 9999;//不可达边的权值

    /**
     * @param: 图结构(邻接矩阵)、源节点的下标(0~节点个数-1)
     * @return 单源最短路径
     */
    public static int[] spfa(int[][] graph, int source){
        //1.邻接矩阵转成邻接表
        int nodenum = graph.length;
        List[] vex = new List[nodenum];
        for(int i=0; i<nodenum; ++i){
            vex[i] = new ArrayList();
        }
        for(int i=0; i<nodenum; ++i){
            for(int j=i+1; j<nodenum; ++j){//假设给定的图是无向图
                if(graph[i][j]!=N){
                    vex[i].add(j);
                    vex[j].add(i);
                }
            }
        }

        //2.spfa
        int[] dist = new int[nodenum];//最短路径数组,初始化为MAX_VALUE
        for(int i=0; i<nodenum; ++i){
            dist[i] = Integer.MAX_VALUE;
        }
        boolean[] used = new boolean[nodenum];//访问标记位
        Queue<Integer> queue = new LinkedList<>();//辅助队列
        int[] num = new int[nodenum];//记录每个节点遍历过的次数,用于检测负向环
        queue.offer(source);//source顶点入队
        dist[source] = 0;//source顶点到自身距离为0
        num[source] = 1;//表示source顶点已被遍历一次
        used[source] = true;
        boolean flag = false;//是否存在负向环
        int[] path = new int[nodenum];//记录最短路径,初始化上一个节点都为source
        for(int i=0; i<nodenum; ++i){
            path[i] = source;
        }
        while (!queue.isEmpty()){
            int u = queue.poll();//获取队头
            used[u] = false;
            for(int i=0; i<vex[u].size(); ++i){//对u的每个相邻节点v进行松弛操作
                int v = (int)vex[u].get(i);
                if( dist[v]>dist[u]+graph[u][v] ){
                    dist[v] = dist[u]+graph[u][v];
                    path[v] = u;
                    if(!used[v]){//如果v没有访问过,入队
                        queue.offer(v);
                        num[v]++;
                        used[v] = true;
                        if(num[v]>=nodenum){//遍历次数大于等于节点数,存在负向环,break
                            flag = true;
                            break;
                        }
                    }
                }
            }
            if(flag){//存在负向环,break
                break;
            }
        }

        if(!flag){//不存在负回路的话打印dist最短路径
            for(int i=0;i<nodenum;i++){
                System.out.println("source:"+source+"...pass:"+path[i]+"-->dest:"+i+",distance:"+dist[i]);
            }
        }
        return dist;
    }

    public static void main(String[] args){
        int[][] graph = new int[][]{
                {0, 1, 5, N},
                {1, 0, 3, 7},
                {5, 3, 0, N},
                {N, 7, N, 0}};
        int[] dist = spfa(graph, 2);//节点0、1、2、3,现求节点2的单源最短路径
        System.out.println(Arrays.toString(dist));
        //out:
        //source:2...pass:1-->dest:0,distance:4
        //source:2...pass:2-->dest:1,distance:3
        //source:2...pass:2-->dest:2,distance:0
        //source:2...pass:1-->dest:3,distance:10
        //[4, 3, 0, 10]
    }
}

4.多源最短路径_Floyd-Warshall算法

时间o(V^3)

最开始只允许经过1号顶点进行中转,接下来只允许经过1和2号顶点进行中转……允许经过1~n号所有顶点进行中转,求任意两点之间的最短路程。用一句话概括就是:从i号顶点到j号顶点只经过前k号点的最短路程。

核心代码:

for(k=1;k<=n;k++)  
	for(i=1;i<=n;i++)  
		for(j=1;j<=n;j++)  
			if(e[i][j]>e[i][k]+e[k][j])  
				e[i][j]=e[i][k]+e[k][j]; 
				

29.Word Ladder(2道)(SPFA最短路算法应用)

Word Ladder--leetcode 127
Word Ladder2--leetcode 126

1.Word Ladder

leetcode 127

题目:Given two words (beginWord and endWord), and a dictionary’s word list, find the length of shortest transformation sequence from beginWord to endWord, such that:

1.Only one letter can be changed at a time.
2.Each transformed word must exist in the word list. Note that beginWord is not a transformed word.

Note:

Return 0 if there is no such transformation sequence.
All words have the same length.
All words contain only lowercase alphabetic characters.
You may assume no duplicates in the word list.
You may assume beginWord and endWord are non-empty and are not the same.

Example 1:

Input:
beginWord = "hit",
endWord = "cog",
wordList = ["hot","dot","dog","lot","log","cog"]

Output: 5

Explanation: As one shortest transformation is "hit" -> "hot" -> "dot" -> "dog" -> "cog",
return its length 5.

题目大意:

思路:最短路径问题。1.构造图结构(邻接表); 2.用SPFA最短路算法求beginWord到endWord的最短改变次数

代码:

//Word Ladder找出beginWord到endWord的最小改变次数。
//思路:最短路问题。1.构图; 2.用SPFA最短路算法求beginWord到endWord的最短改变次数
public int ladderLength(String beginWord, String endWord, List<String> wordList) {
    if(diff(beginWord, endWord)==1){
        return 2;
    }
    //1.构造图的邻接表
    int size = wordList.size();
    List[] edge = new List[size+2];//size装beginWord,size+1装endWord
    for(int i=0; i<edge.length; ++i){
        edge[i] = new ArrayList<Integer>();
    }
    boolean can_arrive = false;//从beginWord到endWord是否能顺利改变
    for(int i=0; i<size; ++i){
        for(int j=i+1; j<size; ++j){
            if(diff(wordList.get(i), wordList.get(j)) == 1){//这两个单词可以转变,图中有边
                edge[i].add(j);
                edge[j].add(i);
            }
        }
        if(diff(beginWord, wordList.get(i))==1){//字典中的当前单词与beginWord可以改变,图中有边
            edge[size].add(i);
        }
        int temp = diff(endWord, wordList.get(i));
        if(temp == 1){//字典中的当前单词与endWord可以改变,图中有边
            edge[i].add(size+1);
        }
        if(temp == 0){//字典中包含endWord,can_arrive
            can_arrive = true;
        }
    }

    if(can_arrive==false){
        return 0;
    }

    //2.上述构好图后,用SPFA最短路算法求beginWord到endWord的最短路径
    int[] dist = new int[size+2];//求edge[size]节点的单源最短路径数组
    for(int i = 0; i<size+2; i++){
        dist[i] = Integer.MAX_VALUE;
    }
    dist[size] = 1;
    Queue<Integer> queue = new LinkedList<>();
    queue.offer(size);//起始节点入队
    while(!queue.isEmpty()) {
        int queFirstNode = queue.poll();   //获取队头
        int dest = dist[queFirstNode];
        for(int i=0; i<edge[queFirstNode].size(); ++i){//遍历queFirstNode为起点的每条边
            //当队头的节点等于边edgeTemp的起点时,进行松弛操作
            int temp = (int)edge[queFirstNode].get(i);
            if( dist[temp] > dest+1 ) {
                dist[temp] = dest+1;
                queue.add(temp);
                if(temp == size+1) {//该节点是终点,即改变成功
                    return dist[temp];//从起点(size)到终点(size+1)的最短距离
                }
            }
        }
    }
    return 0;
}
    
//判断两个字符串不同的字符个数
public int diff(String word1, String word2){
    int res = 0;
    for(int i=0; i<word1.length(); ++i){
        if(word1.charAt(i)!=word2.charAt(i)){
            res++;
        }
    }
    return res;
}

2.Word Ladder2

leetcode 126

题目:题目与上面同,只是结果应该返回所有转换的路径而不是最短的路径长度

思路:还是构造邻接链表+SPFA最短路径算法,当找到终点时用dfs从终点节点反推找出所有不同的最短路径即可。

Example 1:

Input:
beginWord = "hit",
endWord = "cog",
wordList = ["hot","dot","dog","lot","log","cog"]

Output:
[
  ["hit","hot","dot","dog","cog"],
  ["hit","hot","lot","log","cog"]
]

代码:

import java.util.*;
public class Solution {
    List<List<String>> res = new ArrayList<>();
    int size;
    int[] dist;
    List[] edge;
    String a;
    String b;//这些变量弄成全局是为了dfs函数中使用
    List<String> wl;
    ArrayList<String> tempPath = new ArrayList<>();

    public List<List<String>> findLadders(String beginWord, String endWord, List<String> wordList) {
        a = beginWord;
        b = endWord;
        wl = wordList;

        size = wordList.size();
        //1.构造图的邻接表
        edge = new List[size+2];//size装beginWord,size+1装endWord
        for(int i=0; i<edge.length; ++i){
            edge[i] = new ArrayList<Integer>();
        }
        boolean can_arrive = false;//从beginWord到endWord是否能顺利改变
        if(diff(beginWord, endWord)==1){
            edge[size].add(size+1);
            edge[size+1].add(size);
        }
        for(int i=0; i<size; ++i){
            for(int j=i+1; j<size; ++j){
                if(diff(wordList.get(i), wordList.get(j)) == 1){//这两个单词可以转变,图中有边
                    edge[i].add(j);
                    edge[j].add(i);
                }
            }
            if(diff(beginWord, wordList.get(i))==1){//字典中的当前单词与beginWord可以改变,图中有边
                edge[size].add(i);
                edge[i].add(size);
            }
            int temp = diff(endWord, wordList.get(i));
            if(temp == 1){//字典中的当前单词与endWord可以改变,图中有边
                edge[i].add(size+1);
                edge[size+1].add(i);
            }
            if(temp == 0){//字典中包含endWord,can_arrive
                can_arrive = true;
            }
        }

        if(!can_arrive){
            return res;
        }

        //2.上述构好图后,用SPFA最短路算法求beginWord到endWord的最短路径
        dist = new int[size+2];//求edge[size]节点的单源最短路径数组
        for(int i = 0; i<size+2; i++){
            dist[i] = Integer.MAX_VALUE;
        }
        dist[size] = 1;
        Queue<Integer> queue = new LinkedList<>();
        queue.offer(size);//起始节点入队
        while(!queue.isEmpty()) {
            int queFirstNode = queue.poll();   //获取队头
            int dest = dist[queFirstNode];
            for(int i=0; i<edge[queFirstNode].size(); ++i){//遍历queFirstNode为起点的每条边
                //当队头的节点等于边edgeTemp的起点时,进行松弛操作
                int temp = (int)edge[queFirstNode].get(i);
                if( dist[temp] > dest+1 ) {
                    dist[temp] = dest+1;
                    queue.add(temp);
                    if(temp == size+1) {//该节点是终点,即改变成功,用dfs从终点返回回去找路径
                        dfs(temp);
                        return res;
                    }
                }
            }
        }
        return res;
    }

    public void dfs(int p){
        if(p==size){//起点
            tempPath.add(a);
        }
        else if(p==size+1){//终点
            tempPath.add(b);
        }
        else{
            tempPath.add(wl.get(p));
        }

        if(dist[p]==1){//递归出口,如果递归到了起点,说明一条路径已经找到了
            ArrayList<String> temp = new ArrayList<>(tempPath);
            Collections.reverse(temp);
            res.add(temp);
            if (!tempPath.isEmpty()){
                tempPath.remove(tempPath.size()-1);
            }
            return;
        }

        //往回走
        for(int i=0; i<edge[p].size(); ++i){
            int dest = (int)edge[p].get(i);
            if(dist[dest]+1 == dist[p]){
                dfs(dest);
            }
        }
        if (!tempPath.isEmpty()){
            tempPath.remove(tempPath.size()-1);
        }
    }


    //判断两个字符串不同的字符个数
    public int diff(String word1, String word2){
        int res = 0;
        for(int i=0; i<word1.length(); ++i){
            if(word1.charAt(i)!=word2.charAt(i)){
                res++;
            }
        }
        return res;
    }

    public static void main(String[] args){
        Solution s = new Solution();
        String beginWord = "hit";
        String endWord = "cog";
        String[] wordList = new String[]{"hot","dot","dog","lot","log","cog"};

        System.out.println(s.findLadders(beginWord, endWord, Arrays.asList(wordList)));
        
        //out:[[hit, hot, dot, dog, cog], [hit, hot, lot, log, cog]]
    }
}

30.字符串匹配(kmp算法)

leetcode 28

题目:Implement strStr().

Return the index of the first occurrence of needle in haystack, or -1 if needle is not part of haystack.

Example 1:

Input: haystack = "hello", needle = "ll"
Output: 2

Example 2:
Input: haystack = "aaaaa", needle = "bba"
Output: -1

思路:kmp算法。求模式串的next数组,然后进行字符串匹配即可。不用kmp算法之前的时间o(mn),m字符串,n模式串,用kmp算法之后的时候是o(m+n)。下面重点讲解kmp中的next数组怎么求。

如果不写代码的话,kmp的next非常好求,就是考研时候的那个套路,如"ABAD"–>next:[-1,0,0,1],有了next数组,拿模式串j和字符串i依次进行匹配即可,如果不等,则i不动,对应模式串跳转到next[j]继续匹配,如果next[j]==-1,则i也+1即可

代码实现next数组的过程强推油管(KMP) Pattern Matching,下面是自己的总结。

核心就是代码求kmp的next数组如"ABAD"–>next:[-1,0,0,1],见下面的代码:

public int strStr(String haystack, String needle) {
    if(needle==null || needle.length()==0){
        return 0;
    }
    if(haystack==null || haystack.length()==0){
        return -1;
    }
    int i = 0;
    int j = 0;
    int[] next = getNext(needle);

    while (i < haystack.length() && j < needle.length())
    {
        if ( j==-1 || haystack.charAt(i)==needle.charAt(j) )
        {
            i++;
            j++;
        }
        else
            j = next[j];
    }

    if ( j==needle.length() ){//匹配成功,返回needle出现的位置
        return i-j;
    }
    return -1;
}
public int[] getNext(String needle){
    int[] next = new int[needle.length()];
    int j = -1;
    int i = 0;
    next[0] = -1;
    while(i<needle.length()){
        if(j==-1 || needle.charAt(i)==needle.charAt(j)){
            j++;
            i++;
            if(i<needle.length()){
                next[i] = j;
            }
        }
        else{
            j = next[j];
        }
    }
    return next;
}

31.两数相除(转为减法)

leetcode 29

题目:除法运算,但是不能使用/、%、*(除数不会为0)。

思路:将除法转化为减法,但是单纯的做减法计算次数太粗暴,故采用除数扩大(左移),商随之扩大(左移)的思想,同时要考虑正负两边的最大绝对值是不一样的。最简单的方法是用long去处理。另外一个边界条件需要单独处理:-2147483648/-1= 2147483647

把除数表示为:dividend = 2^i * divisor + 2^(i-1) * divisor + … + 2^0 * divisor。这样一来,我们所求的商就是各系数之和了,而每个系数都可以通过移位操作获得。

分两步走:

1)获得i的值;
2)将各系数求和。
显然每步都是logN的复杂度。

代码:

//除法转换为减法。具体思路:除数左移几次,商左移几次。dividend=2^i*divisor+2^(i-1)*divisor+...+2^0*divisor.
public int divide(int dividend, int divisor) {
    if(dividend == 0){
        return 0;
    }
    boolean isPositive = (dividend>0 && divisor>0) || (dividend<0 && divisor<0);
    
    long res = divideDetail( Math.abs((long)dividend), Math.abs((long)divisor) );
    
    if(isPositive && res>Integer.MAX_VALUE){//溢出特例必须处理-2147483648/-1= 2147483647
        res = Integer.MAX_VALUE;
    }
    
    return isPositive ? (int)res : -(int)res;
    
}
    
public long divideDetail(long dividend, long divisor){
    //1.除数左移, 获取i值
    long i = 0;
    while(dividend >= (divisor<<1)){
        divisor <<= 1;
        i++;
    }
    
    //2.商左移cnt次,获取多项式系数和,即商系数相加
    long res = 0;
    while(i>=0){
        if(dividend>=divisor){
            res += (1L<<i);
            dividend -= divisor;
        }
        divisor >>= 1;
        i--;
    }
    
    return res;
}

32.sqrt(x)

leetcode 69

题目:Implement int sqrt(int x).求x的平方根。

Compute and return the square root of x, where x is guaranteed to be a non-negative integer.

Since the return type is an integer, the decimal digits are truncated and only the integer part of the result is returned.

思路1:二分法。定义一个最小精度,用二分法逼近即可。

//二分法
public int mySqrt(int x) {
    if(x == 0){
        return 0;
    }
    if(x<4){
        return 1;
    }

    double left = 1;
    double right = x;
    double mid;
    while(left<=right){
        mid = left + (right-left)/2;
        if(mid*mid==x || right-mid<1e-9){
            return (int)mid;
        }
        else if(mid*mid>x){//因为是double类型所以乘法比除法好,也不会溢出
            right = mid;
        }
        else if(mid*mid<x){
            left = mid;
        }
    }
    return 0;
}

思路1的改进:因为题目中所求是int类型,所以不需要double类型去逼近,用int

//二分法的改进
public int mySqrt(int x) {
    if(x == 0){
        return 0;
    }
    if(x<4){
        return 1;
    }

    int left = 1;
    int right = x;
    int mid;
    while(left<=right){
        mid = left + (right-left)/2;
        if(x/mid==mid){
            return mid;
        }
        else if(x/mid<mid){//因为是int类型所以除法比乘法好,这样也可以避免溢出
            right = mid-1;
        }
        else if(x/mid>mid){
            left = mid+1;
        }
    }
    return right;//while跳出循环时,right < left,应该返回right
}

思路2:牛顿法。对x的平方根的值一个猜想y。通过执行一个简单的操作去得到一个更好的猜测:只需要求出y和x/y的平均值(它更接近实际的平方根值)。

//牛顿法
public int mySqrt(int x) {
    double k=1.0//当然猜想的数拿来主义会更快k=0x5f3759df;
    while(Math.abs(k*k-x)>0.0001) {//精度不能太高,否则太慢
        k=(k+x/k)/2;
    }
    return (int)k;
}

33.乱序的数组中找到最长的递增子序列

leetcode 300

题目:Given an unsorted array of integers, find the length of longest increasing subsequence.

Example:

Input: [10,9,2,5,3,7,101,18]
Output: 4 
Explanation: The longest increasing subsequence is [2,3,7,101], therefore the length is 4. 

思路1(可取,但不够好):DP问题。时间o(n^2)

设长度为N的数组为{a0,a1, a2, …an-1),则假定以aj结尾的数组序列的最长递增子序列长度为L(j),则L(j)={ max(L(i))+1, i。也就是说,我们需要遍历在j之前的所有位置i(从0到j-1),找出满足条件a[i]

例如给定的数组为{5,6,7,1,2,8},则L(0)=1, L(1)=2, L(2)=3, L(3)=1, L(4)=2, L(5)=4。所以该数组最长递增子序列长度为4,序列为{5,6,7,8}。算法代码如下:

//最长递增子序列。思路1:一维DP,时间o(n^2)
public int lengthOfLIS(int[] nums) {
    int len = nums.length;
    int[] longest = new int[len];
    for (int i=0; i<len; i++){
        longest[i] = 1; //初始化都为1
    }

    //dp[j] = {max(L(i))+1}, 如果i
    for (int j=1; j<len; j++) {
        for (int i=0; i<j; i++) {
            if (nums[j]>nums[i] && longest[j]<longest[i]+1){ //注意longest[j]
                longest[j] = longest[i] + 1; //计算以arr[j]结尾的序列的最长递增子序列长度
            }
        }
    }

    //再遍历一遍dp[]数组,取最大的
    int max = 0;
    for (int j=0; j<len; j++) {
        if (longest[j] > max) max = longest[j];  //从longest[j]中找出最大值
    }
    return max;
}

思路2(更好):二分。假设存在一个序列d[1…9] ={ 2,1 ,5 ,3 ,6,4, 8 ,9, 7},可以看出来它的LIS长度为5。

下面一步一步试着找出它。

我们定义一个序列B,然后令 i = 1 to 9 逐个考察这个序列。
此外,我们用一个变量Len来记录现在最长算到多少了

首先,把d[1]有序地放到B里,令B[1] = 2,就是说当只有1一个数字2的时候,长度为1的LIS的最小末尾是2。这时Len=1

然后,把d[2]有序地放到B里,令B[1] = 1,就是说长度为1的LIS的最小末尾是1,d[1]=2已经没用了,很容易理解吧。这时Len=1

接着,d[3] = 5,d[3]>B[1],所以令B[1+1]=B[2]=d[3]=5,就是说长度为2的LIS的最小末尾是5,很容易理解吧。这时候B[1…2] = 1, 5,Len=2

再来,d[4] = 3,它正好加在1,5之间,放在1的位置显然不合适,因为1小于3,长度为1的LIS最小末尾应该是1,这样很容易推知,长度为2的LIS最小末尾是3,于是可以把5淘汰掉,这时候B[1…2] = 1, 3,Len = 2

继续,d[5] = 6,它在3后面,因为B[2] = 3, 而6在3后面,于是很容易可以推知B[3] = 6, 这时B[1…3] = 1, 3, 6,还是很容易理解吧? Len = 3 了噢。

第6个, d[6] = 4,你看它在3和6之间,于是我们就可以把6替换掉,得到B[3] = 4。B[1…3] = 1, 3, 4, Len继续等于3

第7个, d[7] = 8,它很大,比4大,嗯。于是B[4] = 8。Len变成4了

第8个, d[8] = 9,得到B[5] = 9,嗯。Len继续增大,到5了。

最后一个, d[9] = 7,它在B[3] = 4和B[4] = 8之间,所以我们知道,最新的B[4] =7,B[1…5] = 1, 3, 4, 7, 9,Len = 5。

于是我们知道了LIS的长度为5。

注意,这个1,3,4,7,9不是LIS,它只是存储的对应长度LIS的最小末尾。有了这个末尾,我们就可以一个一个地插入数据。虽然最后一个d[9] = 7更新进去对于这组数据没有什么意义,但是如果后面再出现两个数字 8 和 9,那么就可以把8更新到d[5], 9更新到d[6],得出LIS的长度为6。

然后应该发现一件事情了:在B中插入数据是有序的,而且是进行替换而不需要挪动——也就是说,我们可以使用二分查找,将每一个数字的插入时间优化到O(logN)~~~~~于是算法的时间复杂度就降低到了O(NlogN)~!

代码如下(代码中的数组B从位置0开始存数据):

//最长递增子序列。思路2:辅助数组存储有序数列,时间o(nlogn)
public int lengthOfLIS(int[] nums) {
    int[] dp = new int[nums.length];
    int len = 0;
    for(int num : nums){
        //二分查找,查找不到的话就返回负数,负数从1开始。[1,2,3](索引是0,1,2/找不到的索引是1,2,3) 找num=1.5 返回-2
        int i = Arrays.binarySearch(dp, 0, len, num);
        if(i < 0) i = -i-1;
        dp[i] = num;
        if(len == i) len ++;//不是更新的值,而是实实在在的扩张递增序列了,所以len++
    }
    return len;
}

34.蓄水池问题

leetcode 42

左神刷题_第3张图片

题目:Given n non-negative integers representing an elevation map where the width of each bar is 1, compute how much water it is able to trap after raining.

Example:

Input: [0,1,0,2,1,0,1,3,2,1,2,1]
Output: 6

思路:两头双指针往中间。

//蓄水池问题。思路:两头双指针往中间
public int trap(int[] height) {
    int leftmax = 0;//左边最大的板子
    int rightmax = 0;//右边最大的板子
    int a = 0;
    int b = height.length - 1;
    int sum = 0;
    while(a <= b){
        leftmax = Math.max(height[a], leftmax);//每次都更新下左右两边最大的板子
        rightmax = Math.max(height[b], rightmax);
        if(leftmax < rightmax){//左边比右边的板子小,右边能挡住,因此看左边的短板
            sum += leftmax - height[a];
            a++;
        }else{
            sum += rightmax - height[b];
            b--;
        }
    }
    return sum;
}

35.满足指定sum条件的长度最小的子数组

leetcode 209 Minimum Size Subarray Sum

题目:Given an array of n positive integers and a positive integer s, find the minimal length of a contiguous subarray of which the sum ≥ s. If there isn’t one, return 0 instead.

Example:

Input: s = 7, nums = [2,3,1,2,4,3]
Output: 2
Explanation: the subarray [4,3] has the minimal length under the problem constraint.

思路:遍历一遍数组,两个指针变量i,j控制窗口动态变化,时间o(n)

public int minSubArrayLen(int s, int[] nums) {
    if(nums==null || nums.length==0){
        return 0;
    }
    int i = 0;
    int j = 0;
    int resMin = Integer.MAX_VALUE;
    int sum = nums[i];
    while(j<nums.length){
        while(sum < s){//和不够,j++,扩大窗口
            j++;
            if(j>=nums.length){//循环出口
                return resMin==Integer.MAX_VALUE ? 0 : resMin;
            }
            sum += nums[j];
        }
        resMin = Math.min(resMin, j-i+1);//更新最小窗口长度
        while(sum >= s){//和够了,i++,缩小窗口
            resMin = Math.min(resMin, j-i+1);更新最小窗口长度
            sum -= nums[i];
            i++;
        }
    }

    return 0;
}

36.最大值减去最小值小于或等于num的子数组数量

《左神》31

题目:给定数组arr和整数num,共返回有多少个子数组满足子数组的最大值-最小值<=num

思路:数组长度n,时间o(n),空间o(n)

1、生成两个队列qmax和qmin. 两个指针i,j,标定数组边界

2、令j不断向右移动(j++),表示arr[i…j]一直向右扩大,并不断更新qmax和qmin结构,保证qmax和qmin始终维持动态窗口最大值和最小值的更新结构。直到j不满足向右扩张条件时,以arr[i]为左边界且满足条件的子数组(子数组长度>=2)个数为j-i

3、当进行完步骤2,令i向右移动一个位置并对qmax和qmin做出相应的更新做出相应的更新。

4、根据步骤2,步骤3,依次求出以arr[0],arr[1],arr[2]……、arr[N-1]作为第一个元素的子数组中满足条件的数量分别有多少,累加起来起来的数量就是最终的结果。

//最大值减去最小值小于或等于num的子数组数量(子数组长度大于1)。思路:两个队列qmax和qmin. 两个指针i,j
//依次求出以arr[0],arr[1],arr[2]…..、arr[N-1]作为第一个元素的子数组中满足条件的数量
public static int getNum(int[] arr,int num){
    if(arr==null || arr.length<2){
        return 0;
    }
    int i = 0;
    int j = 0;//辅助i/j两个指针
    Deque<Integer> qmax = new LinkedList<>();
    Deque<Integer> qmin = new LinkedList<>();//辅助两个双端队列记录当前子数组的最大/小值
    int res = 0;
    while(i<arr.length){
        while(j<arr.length){//j不断往右扩
            while(!qmax.isEmpty() && arr[j]>=arr[qmax.peekLast()]){//注意这里必须有=
                qmax.pollLast();
            }
            qmax.offerLast(j);
            while(!qmin.isEmpty() && arr[j]<=arr[qmin.peekLast()]){
                qmin.pollLast();
            }
            qmin.offerLast(j);
            if(!qmax.isEmpty()&&!qmin.isEmpty() && arr[qmax.peekFirst()]-arr[qmin.peekFirst()] <= num){
                j++;
            }
            else{//当j不满足继续向右扩的条件时,break并计算当前以arr[i]为底的子数组的个数
                break;
            }
        }
        i++;
        res += j-i;//计算当前以arr[i]为底的子数组的个数
        if(!qmax.isEmpty() && qmax.peekFirst()<i){//将队列窗口之外的元素索引弹出(i之前的都不要了)
            qmax.pollFirst();
        }
        if(!qmin.isEmpty() && qmin.peekFirst()<i){
            qmin.pollFirst();
        }
    }

    return res;
}
public static void main(String[] args){
    int[] nums = new int[]{1,2,3,4,5};

    System.out.println(getNum(nums,1));//out:4  [1,2][2,3][3,4][4,5]
}

37.几道未整理的

###1.有序数组合并###

####问题:

有两个从小到大排序以后的数组A和B,其中A的末端有足够的缓冲空容纳B。请编写一个方法,将B合并入A并排序。
给定两个有序int数组A和B,A中的缓冲空用0填充,同时给定A和B的真实大小int n和int m,请返回合并后的数组。

####思路:

这道题很简单,就是一个归并的过程。和归并排序里面的归并函数做法基本一样,但需要注意的是,这道题是把数组B加入到数组A里。我们需要从后往前比较、加入,这样防止覆盖掉数组A前面的有用部分。过程大致为我们每次从两个列表后面元素选取较大的一个,放入A最后,直到某一个列表到达头部,再将另一个剩下部分逆序取出。时间复杂度O(n+m),空间O(1)。

####代码:

class A
{
    public int[] mergeAB(int[] a, int[] b, int n, int m)
    {
        int pa = n - 1;
        int pb = m - 1;
        int p = m + n - 1;
        do
        {
            if(pa==-1)
                for(int i=p; i>=0; i--)
                    a[i] = b[pb--];
	
            else if(pb==-1)
                return a;
            else
                a[p--] = (a[pa]>b[pb])?a[pa--]:b[pb--];
        }while(p!=0);
    return a;
    }
}
	
public class Example
{
    public static void main(String[] args)
    {
        A cc = new A();
        int[] a = {1,3,5,7,11, 0, 0, 0, 0, 0};
        int[] b = {2,4,6,8,12};
        int[] c = new int[5];
        c = cc.mergeAB(a, b, 5, 5);
        for(int i: c)
            System.out.print(i + " ");
    }
}
//output:1 2 3 4 5 6 7 8 11 12

###2.三色排序

####问题:

有一个只由0,1,2三种元素构成的整数数组,请使用交换、原地排序而不是使用计数进行排序。
给定一个只含0,1,2的整数数组A及它的大小,请返回排序后的数组。保证数组大小小于等于500。

测试样例:[0,1,1,0,2,2],6

返回:[0,0,1,1,2,2]

####思路:

这是一个经典的荷兰国旗问题,处理过程和快排的划分过程相似,可以参考快排的划分技巧。时间复杂度O(n),空间O(1)。过程为:

遍历数组之前,在数组左端设立“0区”,初始大小0,在数组右端设立“2区”,初始大小0。遍历数组,如果是1,直接跳到下一个;如果是0,把当前元素与“0区”后一位交换,“0区”大小+1,遍历下一个元素;遇到2,把当前元素与“2区”前一位交换,“2区”大小+1,由于“2区”元素并没有遍历过,所以不跳到后一个位置,继续遍历该位置元素。

####代码:

public class ThreeColor {
	public int[] sortThreeColor(int[] A, int n) {
        // write code here
        int i=-1;
        int j=n;
        int temp;
        for(int k=0;k<j;){
            if(A[k]==0){
                swap(A,++i,k++); 
            }
            else if(A[k]==2){
                swap(A,--j,k);
            }
            else
                k++;
        }
        return A;
    }
     
    void swap(int A[],int a,int b){
        int temp=A[a];
        A[a]=A[b];
        A[b]=temp;
    }
}

其实拿到这个问题我最先想到的是用计数排序处理,只要三个桶,几乎可以认为是原地的,简单多了。但这里明确说要用交换,而不是计数。在在线课程下面的评论区里,有小伙伴提出和我一样的疑问,老师的回答是:

如果数组里面放的不是int,long这种类型,而是具体一个一个实例呢?你还能压缩在一起吗?比如数组里面放的是“人”这个类的实例,每个实例有一个“身高”的数据项,请把小于160放左边,160~170放中间,170以上放右边。荷兰国旗问题重点介绍的是一种处理数组的技巧。这种技巧从快排中来,掌握了可以解决很多类似的问题。我并不是在强调这么做才对,只是一种技巧而已。

###3.最短子数组问题

####问题:

对于一个数组,请设计一个高效算法计算需要排序的最短子数组的长度。
给定一个int数组A和数组的大小n,请返回一个二元组,代表所求序列的长度。(原序列位置从0开始标号,若原序列有序,返回0)。保证A中元素均为正整数。
测试样例:
[1,4,6,5,9,10],6
返回:2

####思路:
拿到这道题,我最直接的想法就是先排序,再比较排序后的数组有变化的位置,位置有变化的元素里的最左的一个到最右的一个,这之间的数组就是题目要求的需要排序的最短子数组。这种方法需要额外空间复杂度O(n),用来保存排序后的数组(或者保存原数组各元素下标情况,这取决于具体实现)。时间复杂度O(nlogn)。

由上面的这个思路,我们可以想到,其实只要知道,需要调整的元素里最右的元素和最左的元素的位置,就可以得到需要排序的最短子数组的长度。我们知道,如果是有序数组,一定是越往右,数值越大,越往左,数值越小,不满足这个条件的元素,那么就是需要调整的元素。于是可以想到下面的这种处理方法。它可以做到时间复杂度O(n),额外空间复杂度O(1)。处理过程大致为:

先向右遍历,记住遍历过的元素中的最大值max。如果遍历的当前元素i的值A[i]小于max,说明i是需要向左调整的,记住它。向右遍历,只记录需要向左调整的元素的最右的一个,记为R。
再从右至左遍历一次,这次记住遍历过的元素中的最小值min。同理,如果遍历的当前元素i的值A[i]大于min,说明i是需要向右调整的,记住它。遍历过程只记录要调整的最左的一个元素,记为L。A[l]~A[R]就是需要排序的最短子数组,它的长度是R-L+1.

####代码:

public int shortestSubsequence(int[] A, int n) {
    int max = A[0];
    int min = A[n-1];
    int l = -1;
    int r = 0;
	
    //从左至右遍历,记录最右的当前值小于最大值情况
    for(int i=1; i<n; ++i)
    {
        if(A[i]>=max)
            max = A[i];
        else
            l = i;
    }
    //从右至左遍历,记录最左的当前值大于最小值情况
    for(int j=n-2; j>=0; j--)
    {
        if(A[j]<min)
            min = A[j];
        else
            r = j;
    }
    return l-r+1;
}

###4.有序矩阵查找

####问题:

现在有一个行和列都排好序的矩阵,请设计一个高效算法,快速查找矩阵中是否含有值x。
给定一个int矩阵mat,同时给定矩阵大小nxm及待查找的数x,请返回一个bool值,代表矩阵中是否存在x。所有矩阵中数字及x均为int范围内整数。保证n和m均小于等于1000。

测试样例:

[[1,2,3],[4,5,6],[7,8,9]],3,3,10

返回:false

####思路:

这道题可以做到时间复杂度O(m+n),额外空间复杂度O(1)。用下面这个矩阵举例说明。

左神刷题_第4张图片

我们从右上角或左下角作为起始位置开始遍历。这么做是因为矩阵行列都是有序的,右上角是行最小,列最大,左下角相反。我们这里选择从右上角开始,假设待查值是3。当前值是5,如果待查值比当前值大,那么往下走一步,因为我们知道这一行当前位置是最大的,左面所有元素都小于该值,就不用考虑;如果待查值更小,那么往左走一步,理由同上;如果相等,返回true。待查值3<当前值5,往左走一步,当前值变成2。重复上面过程,当前值=4。3<4,所以再往左走,现在待查值3=当前值3,返回true。如果直到越界都没找到,则返回false。

####代码:

boolean findX(int[][] mat, int n, int m, int x)
{
    for(int i=0, j=m-1; (i!=n)&&(j!=-1); ) //起始位置(i,j)从矩阵右上角开始
    {
        if(x>mat[i][j]) //目标值大于矩阵元素,向下走
            i++;
        else if(x < mat[i][j])  //目标值小于矩阵元素,向左走
            j--;
        else if(x == mat[i][j])
            return true;
    }
    return false;
}

你可能感兴趣的:(算法刷题)