算法题库-剑指Offer

算法题库-剑指Offer-01~30题

(三)⭐️链表

(1)《剑指offer》3-倒着返回一个链表【栈、递归、首位插入】

(1)题目描述: 输入一个链表,按链表从尾到头的顺序返回一个ArrayList(或者数组)。

(2)解法一:栈
listNode 是链表,只能从头遍历到尾,但是输出却要求从尾到头,这是典型的"先进后出",我们可以想到栈!
ArrayList 中有个方法是 add(index,value),可以指定 index 位置插入 value 值
所以我们在遍历 listNode 的同时将每个遇到的值插入到 list 的 0 位置,最后输出 listNode 即可得到逆序链表

public class Solution {
    public ArrayList<Integer> printListFromTailToHead(ListNode listNode) {
        ArrayList<Integer> list=new ArrayList<>();
        ListNode tmp=listNode;
        while(tmp!=null){
            list.add(0,tmp.val);
            tmp=tmp.next;
        }
        return list;
    }
}

时间复杂度:O(n)
空间复杂度:O(n)

(3)解法二:用递归方法

public class Solution {
    ArrayList<Integer> list=new ArrayList();
    public ArrayList<Integer> printListFromTailToHead(ListNode listNode) {
        if(listNode!=null){
            printListFromTailToHead(listNode.next);
            list.add(listNode.val);
        }
        return list;
    }
}

时间复杂度:O(n)
空间复杂度:O(n)

(4)解法三:列表的首位插入
每次插入数据,都总是插入到首位,这样得到的List就是从尾到头的链表序列。

//方法三:列表的首位插入
public ArrayList<Integer> printListFromTailToHead(ListNode listNode){
    ArrayList<Integer> list=new ArrayList<>();
    if(listNode==null)
        return list;
    ListNode head=listNode;
    while(head!=null){ 
        list.add(0,head.val);  //每次插入数据,都总是插入到首位
        head=head.next;
    }
    return list;
}

(2)《剑指offer》14-链表中倒数第k个节点【栈、递归、双指针】

(1)题目描述:
输入一个链表,输出该链表中倒数第k个结点。为了符合习惯,从1开始计数,即链表的尾结点是倒数第1个节点。例如,一个链表有6个结点,从头结点开始,它们的值依次是1,2,3,4,5,6。则这个链表倒数第三个结点是值为4的结点。

例如输入{1,2,3,4,5},2时,对应的链表结构如下图所示:
在这里插入图片描述其中蓝色部分为该链表的最后2个结点,所以返回倒数第2个结点(也即结点值为4的结点)即可,系统会打印后面所有的节点来比较。

(2)解法一:双指针
对于单链表来说,没有从后向前的指针,因此一个直观的解法是先进行一次遍历,统计出链表中结点的个数n,第二次再进行一次遍历,找到第n-k+1个结点就是我们要找的结点,但是这需要对链表进行两次遍历。

为了实现一次遍历,我们这里采用双指针解法。我们可以定义两个指针,第一个指针从链表的头指针开始先向前走k步,第二个指针保持不动,从第k+1步开始,第二个指针也从头开始前进,两个指针都每次前进一步。这样,两个指针的距离都一直保持在k,当快指针(走在前面的)到达null时,慢指针(走在后面的)正好到达第k个结点。注意:要时刻留意空指针的判断。

在这里插入图片描述

public class Solution {
    public ListNode FindKthToTail(ListNode head,int k) {
        //先判断头指针不为空,k不为0
        if(head == null || k ==0 ){
            return null;
        }
 
        //两个指针刚开始都指向头部,一个快一个慢
        ListNode slow=head;
        ListNode fast=head;
        //遍历前k个
        for(int i=0;i<k;i++){
            if(fast==null){
                return null;
            }
            //如果快指针不为空,就后移一位
            fast=fast.next;
            //如果长度不到k,那就返回最后一个节点
            //否则,最后会返回第k个节点。所以fast指针会指向第k个节点
        }
        //当快指针不为空的话,快指针和慢指针同时往后移一位,直到快指针为空
        while(fast!=null){
            slow=slow.next;
            fast=fast.next;
        }
 
        //最后返回慢指针指向的节点,快指针为空,而慢指针指向倒数第k个位置
        return slow;
    }
}

思路分析:
使用两个指针,先让fast指向第k个,slow指向第1个。
然后fast和slow同时后移,最后fast指向最后一个,slow指向倒数第k个

(3)解法二:双指针
这题要求链表的倒数第k个节点,最简单的方式就是使用两个指针,第一个指针先移动k步,然后第二个指针再从头开始,这个时候这两个指针同时移动,当第一个指针到链表的末尾的时候,返回第二个指针即可。注意,如果第一个指针还没走k步的时候链表就为空了,我们直接返回null即可。
在这里插入图片描述

public ListNode FindKthToTail(ListNode pHead, int k) {
    if (pHead == null)
        return pHead;
    ListNode first = pHead;
    ListNode second = pHead;
    //第一个指针先走k步
    while (k-- > 0) {
        if (first == null)
            return null;
        first = first.next;
    }
    //然后两个指针在同时前进
    while (first != null) {
        first = first.next;
        second = second.next;
    }
    return second;
}

(4)解法三:栈
这题要求的是返回后面的k个节点,我们只要把原链表的结点全部压栈,然后再把栈中最上面的k个节点出栈,出栈的结点重新串成一个新的链表即可,原理也比较简单,直接看下代码。

public ListNode FindKthToTail(ListNode pHead, int k) {
    Stack<ListNode> stack = new Stack<>();
    //链表节点压栈
    int count = 0;
    while (pHead != null) {
        stack.push(pHead);
        pHead = pHead.next;
        count++;
    }
    if (count < k || k == 0)
        return null;
    //在出栈串成新的链表
    ListNode firstNode = stack.pop();
    while (--k > 0) {
        ListNode temp = stack.pop();
        temp.next = firstNode;
        firstNode = temp;
    }
    return firstNode;
}

(5)解法四:递归求解
之前讲过链表的逆序打印410,剑指 Offer-从尾到头打印链表,其中有这样一段代码

public void reversePrint(ListNode head) {
    if (head == null)
        return;
    reversePrint(head.next);
    System.out.println(head.val);
}

这段代码其实很简单,我们要理解他就要弄懂递归的原理,递归分为两个过程,递和归,看一下下面的图就知道了,先往下传递,当遇到终止条件的时候开始往回走。
在这里插入图片描述

递归的模板

public ListNode getKthFromEnd(ListNode head, int k) {
    //终止条件
    if (head == null)
        return head;
    //递归调用
    ListNode node = getKthFromEnd(head.next, k);
    //逻辑处理
    ……
}

终止条件很明显就是当节点head为空的时候,就没法递归了,这里主要看的是逻辑处理部分,当递归往下传递到最底端的时候,就会触底反弹往回走,在往回走的过程中记录下走过的节点,当达到k的时候,说明到达的那个节点就是倒数第k个节点,直接返回即可,如果没有达到k,就返回空,搞懂了上面的过程,代码就很容易写了

//全局变量,记录递归往回走的时候访问的结点数量
int size;

public ListNode FindKthToTail(ListNode pHead, int k) {
    //边界条件判断
    if (pHead == null)
        return pHead;
    ListNode node = FindKthToTail(pHead.next, k);
    ++size;
    //从后面数结点数小于k,返回空
    if (size < k) {
        return null;
    } else if (size == k) {
        //从后面数访问结点等于k,直接返回传递的结点k即可
        return pHead;
    } else {
        //从后面数访问的结点大于k,说明我们已经找到了,
        //直接返回node即可
        return node;
    }
}

上面代码在仔细一看,当size小于k的时候node节点就是空,所以我们可以把size大于k和小于k合并为一个,这样代码会更简洁一些

int size;

public ListNode FindKthToTail(ListNode pHead, int k) {
    if (pHead == null)
        return pHead;
    ListNode node = FindKthToTail(pHead.next, k);
    if (++size == k)
        return pHead;
    return node;
}

(3)《剑指offer》15-反转链表【栈、递归、双链表、指针】

(1)题目描述:
输入一个链表,反转链表后,输出新链表的表头。

(2)解题思路:
本题比较简单,有两种方法可以实现:(1)三指针。使用三个指针,分别指向当前遍历到的结点、它的前一个结点以及后一个结点。将指针反转后,三个结点依次前移即可。(2)递归方法。同样可以采用递归来实现反转。将头结点之后的链表反转后,再将头结点接到尾部即可。(3)栈

(3)解法一:栈
简单的一种方式就是使用栈,因为栈是先进后出的。实现原理就是把链表节点一个个入栈,当全部入栈完之后再一个个出栈,出栈的时候在把出栈的结点串成一个新的链表。原理如下
在这里插入图片描述

import java.util.Stack;
public class Solution {
	public ListNode ReverseList(ListNode head) {
	    Stack<ListNode> stack= new Stack<>();
	    //把链表节点全部摘掉放到栈中
	    while (head != null) {
	        stack.push(head);
	        head = head.next;
	    }
	    if (stack.isEmpty())
	        return null;
	    ListNode node = stack.pop();
	    ListNode dummy = node;
	    //栈中的结点全部出栈,然后重新连成一个新的链表
	    while (!stack.isEmpty()) {
	        ListNode tempNode = stack.pop();
	        node.next = tempNode;
	        node = node.next;
	    }
	    //最后一个结点就是反转前的头结点,一定要让他的next
	    //等于空,否则会构成环
	    node.next = null;
	    return dummy;
	}
}

(4)解法二:指针

public class Solution {
    public ListNode ReverseList(ListNode head) {
         // 判断链表为空或长度为1的情况
        if(head == null || head.next == null){
            return head;
        }
        // 当前节点的前一个节点
        ListNode pre = null; 
        // 当前节点的下一个节点
        ListNode next = null; 
        // 当前节点不为空的时候
        while( head != null ){
            // 记录当前节点的下一个节点位置;
            next = head.next; 
            head.next = pre; // 让当前节点指向前一个节点位置,完成反转
            pre = head; // pre 往右走
            head = next;// 当前节点往右继续走
        }
        return pre;
    }
}

(5)解法三:递归
我们再来回顾一下递归的模板,终止条件,递归调用,逻辑处理。

public ListNode reverseList(参数0) {
    if (终止条件)
        return;
        
    逻辑处理(可能有,也可能没有,具体问题具体分析)

    //递归调用
    ListNode reverse = reverseList(参数1);

    逻辑处理(可能有,也可能没有,具体问题具体分析)
}

终止条件就是链表为空,或者是链表没有尾结点的时候,直接返回

if (head == null || head.next == null)
    return head;

代码实例

public ListNode ReverseList(ListNode head) {
    //终止条件
    if (head == null || head.next == null)
        return head;
    //保存当前节点的下一个结点
    ListNode next = head.next;
    //从当前节点的下一个结点开始递归调用
    ListNode reverse = ReverseList(next);
    //reverse是反转之后的链表,因为函数reverseList
    // 表示的是对链表的反转,所以反转完之后next肯定
    // 是链表reverse的尾结点,然后我们再把当前节点
    //head挂到next节点的后面就完成了链表的反转。
    next.next = head;
    //这里head相当于变成了尾结点,尾结点都是为空的,
    //否则会构成环
    head.next = null;
    return reverse;
}

因为递归调用之后head.next节点就会成为reverse节点的尾结点,我们可以直接让head.next.next = head;,这样代码会更简洁一些,看下代码

public ListNode ReverseList(ListNode head) {
    if (head == null || head.next == null)
        return head;
    ListNode reverse = ReverseList(head.next);
    head.next.next = head;
    head.next = null;
    return reverse;
}

这种递归往下传递的时候基本上没有逻辑处理,当往回反弹的时候才开始处理,也就是从链表的尾端往前开始处理的。我们还可以再来改一下,在链表递归的时候从前往后处理,处理完之后直接返回递归的结果,这就是所谓的尾递归,这种运行效率要比上一种好很多

public ListNode ReverseList(ListNode head) {
    return reverseListInt(head, null);
}

private ListNode reverseListInt(ListNode head, ListNode newHead) {
    if (head == null)
        return newHead;
    ListNode next = head.next;
    head.next = newHead;
    return reverseListInt(next, head);
}

尾递归虽然也会不停的压栈,但由于最后返回的是递归函数的值,所以在返回的时候都会一次性出栈,不会一个个出栈这么慢。但如果我们再来改一下,像下面代码这样又会一个个出栈了

public ListNode ReverseList(ListNode head) {
    return reverseListInt(head, null);
}

private ListNode reverseListInt(ListNode head, ListNode newHead) {
    if (head == null)
        return newHead;
    ListNode next = head.next;
    head.next = newHead;
    ListNode node = reverseListInt(next, head);
    return node;
}

(6)解法四:双链表求解
双链表求解是把原链表的结点一个个摘掉,每次摘掉的链表都让他成为新的链表的头结点,然后更新新链表。下面以链表1→2→3→4为例画个图来看下。
在这里插入图片描述

public ListNode ReverseList(ListNode head) {
    //新链表
    ListNode newHead = null;
    while (head != null) {
        //先保存访问的节点的下一个节点,保存起来
        //留着下一步访问的
        ListNode temp = head.next;
        //每次访问的原链表节点都会成为新链表的头结点,
        //其实就是把新链表挂到访问的原链表节点的
        //后面就行了
        head.next = newHead;
        //更新新链表
        newHead = head;
        //重新赋值,继续访问
        head = temp;
    }
    //返回新链表
    return newHead;
}

(4)《剑指offer》16-合并两个排序的链表【递归】

(1)题目描述:
输入两个单调递增的链表,输出两个链表合成后的链表,当然我们需要合成后的链表满足单调不减规则。

(2)解题思路:
首先需要判断几个特殊情况,即判断输入的两个指针是否为空。如果第一个链表为空,则直接返回第二个链表;如果第二个链表为空,则直接返回第一个链表。如果两个链表都是空链表,合并的结果是得到一个空链表。

两个链表都是排序好的,我们只需要从头遍历链表,判断当前指针,哪个链表中的值小,即赋给合并链表指针,剩余的结点仍然是排序的,所以合并的步骤和之前是一样的,所以这是典型的递归过程,用递归可以轻松实现。

在这里插入图片描述
(3)解法一:

public class Solution {
    public ListNode Merge(ListNode list1,ListNode list2) {
        ListNode h = new ListNode(-1);
        ListNode cur = h;
        while(list1 != null && list2 !=null){
            if(list1.val<=list2.val){
                cur.next = list1;
                list1 = list1.next;
            }else{
                cur.next = list2;
                list2 = list2.next;
            }
            cur = cur.next;
        }
        if(list1!=null) cur.next = list1;
        if(list2!=null) cur.next = list2;
        return h.next;
    }
}

(4)解法二:递归
简单地理一下思路:

  • 从头结点开始考虑,比较两表头结点的值,值较小的list的头结点后面接merge好的链表(进入递归了);
  • 若两链表有一个为空,返回非空链表,递归结束;
  • 当前层不考虑下一层的细节,当前层较小的结点接上该结点的next与另一结点merge好的表头就ok了;
  • 每层返回选定的较小结点就ok;

重新整理一下:

  • 终止条件:两链表其中一个为空时,返回另一个链表;
  • 当前递归内容:若list1.val <= list2.val 将较小的list1.next与merge后的表头连接,即list1.next = Merge(list1.next,list2); list2.val较大时同理;
  • 每次的返回值:排序好的链表头;

复杂度:O(m+n) O(m+n)

//方法一:递归实现
public ListNode Merge(ListNode list1,ListNode list2) {
	if(list1==null)
        return list2;
    if(list2==null)
        return list1;
    ListNode head=null;    //头节点
    if(list1.val<=list2.val){
        head=list1;
        head.next=Merge(list1.next,list2);
    }else{
        head=list2;
        head.next=Merge(list1,list2.next);
    }
    return head;
}

空间O(1)的思路:

  • 创建一个虚拟结点和一个哨兵结点
  • 当list1与list2都不为null时循环
  • 哪个的val小哪个赋给虚拟结点的next,虚拟结点后移。
  • 退出循环后,哪个list不为空,哪个结点(包括剩下的)给虚拟结点的next
  • 最后返回哨兵结点的next
public class Solution {
    public ListNode Merge(ListNode list1,ListNode list2) {
        ListNode dummy = new ListNode(-1);
        ListNode res = dummy;
        // 必须保证两个list都不为空
        while(list1 != null & list2 != null) {
            if(list1.val > list2.val) {
                dummy.next = list2;
                list2 = list2.next;
                dummy = dummy.next;
            } else if(list1.val <= list2.val) {
                dummy.next = list1;
                list1 = list1.next;
                dummy = dummy.next;
            }
        }
        // list1后面还有,就把剩下的全部拿走
        if(list1 != null) {
            dummy.next = list1;
        }
        if(list2 != null) {
            dummy.next = list2;
        }
        return res.next;
    }
}

(5)解法三:非递归实现

//方法二:非递归实现
public ListNode Merge(ListNode list1,ListNode list2) {
	if(list1==null)
        return list2;
    if(list2==null)
        return list1;
    ListNode head=new ListNode(-1);//头节点
    ListNode thehead=head;
    while(list1!=null && list2!=null){
        if(list1.val<=list2.val){
            thehead.next=list1;
            list1=list1.next;
        }else{
            thehead.next=list2;
            list2=list2.next;
        }
        thehead=thehead.next;
    }
    if(list1!=null)  //归并完需要检查哪个链表还有剩余
        thehead.next=list1;
    if(list2!=null)
        thehead.next=list2;
    return head.next;
}

(6)解法四:借助额外数组
1-创建额外存储数组 nums
2-依次循环遍历 pHead1, pHead2,将链表中的元素存储到 nums中,再对nums进行排序
3-依次对排序后的数组 nums取数并构建合并后的链表
在这里插入图片描述

public class Solution {
    public ListNode Merge(ListNode list1,ListNode list2) {
        // list1 list2为空的情况
        if(list1==null) return list2;
        if(list2==null) return list1;
        if(list1 == null && list2 == null){
            return null;
        }
        //将两个两个链表存放在list中
        ArrayList<Integer> list = new ArrayList<>();
        // 遍历存储list1
        while(list1!=null){
            list.add(list1.val);
            list1 = list1.next;
        }
        // 遍历存储list2
        while(list2!=null){
            list.add(list2.val);
            list2 = list2.next;
        }
        // 对 list 排序
        Collections.sort(list);
        // 将list转换为 链表
        ListNode newHead = new ListNode(list.get(0));
        ListNode cur = newHead;
        for(int i=1;i<list.size();i++){
            cur.next = new ListNode(list.get(i));
            cur = cur.next;
        }
        // 输出合并链表
        return newHead;
    }
}

时间复杂度O(N+M):M N分别表示pHead1, pHead2的长度,依次遍历链表
空间复杂度O(N+M):额外存储数组占用空间

(5)《剑指offer》25-复杂链表的复制【模拟+哈希表】

(1)题目描述:
输入一个复杂链表(每个节点中有节点值,以及两个指针,一个指向下一个节点,另一个特殊指针指向任意一个节点),返回结果为复制后复杂链表的head。(注意,输出结果中请不要返回参数中的节点引用,否则判题程序会直接返回空)
在这里插入图片描述

示例:
输入:{1,2,3,4,5,3,5,#,2,#}
输出:{1,2,3,4,5,3,5,#,2,#}
解析:我们将链表分为两段,前半部分{1,2,3,4,5}为ListNode,后半部分{3,5,#,2,#}是随机指针域表示。
以上示例前半部分可以表示链表为的ListNode:1->2->3->4->5
后半部分,3,5,#,2,#分别的表示为
1的位置指向3,2的位置指向5,3的位置指向null,4的位置指向2,5的位置指向null
如下图:
在这里插入图片描述

(2)解题思路
本题有以下三种解法:

第一种:先按照next复制,然后依次添加random指针,添加时需要定位random的位置,定位一次需要一次遍历,需要O(n^2)的复杂度。

第二种:先按照next复制,然后用一个hashmap保存原节点和复制后节点的对应关系,则用O(n)的空间复杂度使时间复杂度降到了O(n)。

第三种(最优方法):同样先按next复制,但是把复制后的节点放到原节点后面,则可以很容易的添加random,最后按照奇偶位置拆成两个链表,时间复杂度O(n),不需要额外空间。
在这里插入图片描述

/*
public class RandomListNode {
    int label;
    RandomListNode next = null;
    RandomListNode random = null;

    RandomListNode(int label) {
        this.label = label;
    }
}
*/

/*
*解题思路:
*1、遍历链表,复制每个结点,如复制结点A得到A1,将结点A1插到结点A后面;
*2、重新遍历链表,复制老结点的随机指针给新结点,如A1.random = A.random.next;
*3、拆分链表,将链表拆分为原链表和复制后的链表
*/
public class Solution {
    public RandomListNode Clone(RandomListNode pHead)
    {

        if(pHead == null) {
            return null;
        }
 
        RandomListNode currentNode = pHead;
        //1、复制每个结点,如复制结点A得到A1,将结点A1插到结点A后面;
        while(currentNode != null){
            RandomListNode cloneNode = new RandomListNode(currentNode.label);
            RandomListNode nextNode = currentNode.next;
            currentNode.next = cloneNode;
            cloneNode.next = nextNode;
            currentNode = nextNode;
        }
 
        currentNode = pHead;
        //2、重新遍历链表,复制老结点的随机指针给新结点,如A1.random = A.random.next;
        while(currentNode != null) {
            currentNode.next.random = currentNode.random==null?null:currentNode.random.next;
            currentNode = currentNode.next.next;
        }
 
        //3、拆分链表,将链表拆分为原链表和复制后的链表
        currentNode = pHead;
        RandomListNode pCloneHead = pHead.next;
        while(currentNode != null) {
            RandomListNode cloneNode = currentNode.next;
            currentNode.next = cloneNode.next;
            cloneNode.next = cloneNode.next==null?null:cloneNode.next.next;
            currentNode = currentNode.next;
        }
 
        return pCloneHead;
    }
}

换一种写法:

public class Solution {
    public RandomListNode Clone(RandomListNode pHead){
        if(pHead==null)
            return null;
        //(1)先按next复制,但是把复制后的节点放到对应原节点后面
        CopyNodes(pHead);
        //(2)依次添加random指针
        addRandom(pHead);
        //(3)按照奇偶位置拆成两个链表
        return ReconnectNodes(pHead);
    }
    //先按next复制,但是把复制后的节点放到对应原节点后面
    public void CopyNodes(RandomListNode pHead){
        RandomListNode head=pHead;
        while(head!=null){
            RandomListNode temp=new RandomListNode(head.label);
            temp.next=head.next; //复制一个结点,插在对应的原节点的后面
            temp.random=null;
            head.next=temp;
            head=temp.next;
        }
    }
    //依次添加random指针
    public void addRandom(RandomListNode pHead){
        RandomListNode head=pHead;
        while(head!=null){
            RandomListNode head_new=head.next;
            if(head.random!=null)
                head_new.random=head.random.next;
            head=head_new.next;
        }
    }
    //按照奇偶位置拆成两个链表
    public RandomListNode ReconnectNodes(RandomListNode pHead){
        if(pHead==null)
            return null;
        RandomListNode head=pHead;
        RandomListNode pHeadClone=head.next;	//pHeadClone 是我们需要返回的头指针,不能再这个基础上操作,所以下一行要用一个新的指针指向它来操作。说明:head是对pHead的复制,headClone是对pHeadClone的复制
        RandomListNode headClone=pHeadClone;
        while(head!=null){
            head.next=headClone.next;			//代入例子理解,这一行的作用即A->B
            head=head.next;						//把head的指针指向B
             if(head!=null){
                headClone.next=head.next;		//A‘->B’
                headClone=headClone.next;		//把headClone的指针指向B'
             }
        }
        return pHeadClone;
    }
}

(3)解法三:模拟+哈希表
如果不考虑 random 指针的话,对一条链表进行拷贝,我们只需要使用两个指针:一个用于遍历原链表,一个用于构造新链表(始终指向新链表的尾部)即可。这一步操作可看做是「创建节点 + 构建 next 指针关系」。

现在在此基础上增加一个 random 指针,我们可以将 next 指针和 random 指针关系的构建拆开进行:

1- 先不考虑 random 指针,和原本的链表复制一样,创建新新节点,并构造 next 指针关系,同时使用「哈希表」记录原节点和新节点的映射关系;
2- 对原链表和新链表进行同时遍历,对于原链表的每个节点上的 random 都通过「哈希表」找到对应的新 random 节点,并在新链表上构造 random 关系。

import java.util.*;
public class Solution {
    public RandomListNode Clone(RandomListNode head) {
        Map<RandomListNode, RandomListNode> map = new HashMap<>();
        RandomListNode dummy = new RandomListNode(-1);
        RandomListNode tail = dummy, tmp = head;
        while (tmp != null) {
            RandomListNode node = new RandomListNode(tmp.label);
            map.put(tmp, node);
            tail.next = node;
            tail = tail.next;
            tmp = tmp.next;
        }
        tail = dummy.next;
        while (head != null) {
            if (head.random != null) tail.random = map.get(head.random);
            tail = tail.next;
            head = head.next;
        }
        return dummy.next;
    }
}

时间复杂度:O(n)
空间复杂度:O(n)

(4)解法四:模拟(原地算法)
显然时间复杂度上无法优化,考虑如何降低空间(不使用「哈希表」)。

我们使用「哈希表」的目的为了实现原节点和新节点的映射关系,更进一步的是为了快速找到某个节点 random 在新链表的位置。

那么我们可以利用原链表的 next 做一个临时中转,从而实现映射。

具体的,我们可以按照如下流程进行:

  1. 对原链表的每个节点节点进行复制,并追加到原节点的后面;
  2. 完成 操作之后,链表的奇数位置代表了原链表节点,链表的偶数位置代表了新链表节点,且每个原节点的 next 指针执行了对应的新节点。这时候,我们需要构造新链表的 random 指针关系,可以利用 link[i + 1].random = link[i].random.next, 为奇数下标,含义为 新链表节点的 random 指针指向旧链表对应节点的 random 指针的下一个值;
  3. 对链表进行拆分操作。

在这里插入图片描述

import java.util.*;
public class Solution {
    public RandomListNode Clone(RandomListNode head) {
        if (head == null) return null;
        RandomListNode dummy = new RandomListNode(-1);
        dummy.next = head;
        while (head != null) {
            RandomListNode node = new RandomListNode(head.label);
            node.next = head.next;
            head.next = node;
            head = node.next;
        }
        head = dummy.next;
        while (head != null) {
            if (head.random != null) {
                head.next.random = head.random.next;
            }
            head = head.next.next;
        }
        head = dummy.next;
        RandomListNode ans = head.next;
        while (head != null) {
            RandomListNode tmp = head.next;
            if (head.next != null) head.next = head.next.next;
            head = tmp;
        }
        return ans;
    }
}

时间复杂度:O(n)
空间复杂度:O(1)

(6)《剑指offer》36 两个链表的第一个公共结点【栈、Set等】

(1)题目描述:
输入两个无环的单向链表,找出它们的第一个公共结点,如果没有公共节点则返回空。(注意因为传入数据是链表,所以错误测试数据的提示是用其他方式显示的,保证传入数据是正确的)

(2)解法一:栈

本题首先可以很直观的想到蛮力法,即对链表1(假设长度为m)的每一个结点,遍历链表2(假设长度为n),找有没有与其相同的结点,这显然复杂度为O(mn)。

进一步考虑,我们可以得到以下三种改进的解法:

方法一:借助辅助栈。我们可以把两个链表的结点依次压入到两个辅助栈中,这样两个链表的尾结点就位于两个栈的栈顶,接下来比较两个栈顶的结点是否相同。如果相同,则把栈顶弹出继续比较下一个,直到找到最后一个相同的结点。此方法也很直观,时间复杂度为O(m+n),但使用了O(m+n)的空间,相当于用空间换区了时间效率的提升。

方法二:将两个链表设置成一样长。具体做法是先求出两个链表各自的长度,然后将长的链表的头砍掉,也就是长的链表先走几步,使得剩余的长度与短链表一样长,这样同时向前遍历便可以得到公共结点。时间复杂度为O(m+n),不需要额外空间。

方法三:将两个链表拼接起来。 将两个链表进行拼接,一个链表1在前链表2在后,另一个链表2在前链表1在后,则合成的两个链表一样长,然后同时遍历两个链表,就可以找到公共结点,时间复杂度同样为O(m+n)。

在这里插入图片描述

    public ListNode getIntersectionNode(ListNode pHead1, ListNode pHead2) {
                //方法3
        if(pHead1==null || pHead2==null)
            return null;
        ListNode head1=pHead1,head2=pHead2;
        //输入的head1和head2分别是两个不同的链表,然后进入循环
        //然后比较head1和head2,一开始head1和head2不可能相等,所以得到的值是后者,即head1=head1.next,head2=head2.next,不断比较下一个
        //直到两个链表都到了末端,这时候head1接上head2的链表,head2的链表,同时在while的循环条件中比较,直到两个链表的结点相等,如果不等还是head1=head1.next,       /       //head2=head2.next,不断比较下一个,直到相等
        while(head1!=head2){
            head1 = head1!=null ? head1.next:pHead2;
            head2 = head2!=null ? head2.next:pHead1;
        }
        return head1;
    }

(3)解法二:栈
这是一种「从后往前」找的方式。

将两条链表分别压入两个栈中,然后循环比较两个栈的栈顶元素,同时记录上一位栈顶元素。

当遇到第一个不同的节点时,结束循环,上一位栈顶元素即是答案。

import java.util.*;
public class Solution {
    public ListNode FindFirstCommonNode(ListNode a, ListNode b) {
        Deque<ListNode> d1 = new ArrayDeque<>(), d2 = new ArrayDeque<>();
        while (a != null) {
            d1.add(a);
            a = a.next;
        }
        while (b != null) {
            d2.add(b);
            b = b.next;
        }
        ListNode ans = null;
        while (!d1.isEmpty() && !d2.isEmpty() && d1.peekLast() == d2.peekLast()) {
            ans = d1.pollLast();
            d2.pollLast();
        }
        return ans;
    }
}

时间复杂度:O(n+m)
空间复杂度:O(n+m)

(4)解法三:Set
这是一种「从前往后」找的方式。

使用 Set 数据结构,先对某一条链表进行遍历,同时记录下来所有的节点。

然后在对第二链条进行遍历时,检查当前节点是否在 Set 中出现过,第一个在 Set 出现过的节点即是交点。

import java.util.*;
public class Solution {
    public ListNode FindFirstCommonNode(ListNode a, ListNode b) {
        Set<ListNode> set = new HashSet<>();
        while (a != null) {
            set.add(a);
            a = a.next;
        }
        while (b != null && !set.contains(b)) b = b.next;
        return b;
    }
}

时间复杂度:O(n+m)
空间复杂度:O(n)

(5)解法四:差值法
由于两条链表在相交节点后面的部分完全相同,因此我们可以先对两条链表进行遍历,分别得到两条链表的长度,并计算差值 d。

让长度较长的链表先走 d 步,然后两条链表同时走,第一个相同的节点即是节点。

import java.util.*;
public class Solution {
    public ListNode FindFirstCommonNode(ListNode a, ListNode b) {
        int c1 = 0, c2 = 0;
        ListNode ta = a, tb = b;
        while (ta != null && c1++ >= 0) ta = ta.next;
        while (tb != null && c2++ >= 0) tb = tb.next;
        int d = c1 - c2;
        if (d > 0) {
            while (d-- > 0) a = a.next;
        } else if (d < 0) {
            d = -d;
            while (d-- > 0) b = b.next;
        }
        while (a != b) {
            a = a.next;
            b = b.next;
        }
        return a;
    }
}

时间复杂度:O(n+m)
空间复杂度:O(1)

(6)解法五:等值法
这是「差值法」的另外一种实现形式,原理同样利用「两条链表在相交节点后面的部分完全相同」。

我们令第一条链表相交节点之前的长度为 a,第二条链表相交节点之前的长度为 b,相交节点后的公共长度为 c(注意 c 可能为 ,即不存在相交节点)。

分别对两条链表进行遍历:

当第一条链表遍历完,移动到第二条链表的头部进行遍历;
当第二条链表遍历完,移动到第一条链表的头部进行遍历。
如果存在交点:第一条链表首次到达「第一个相交节点」的充要条件是第一条链表走了 步,由于两条链表同时出发,并且步长相等,因此当第一条链表走了 步时,第二条链表同样也是走了 步,即 第二条同样停在「第一个相交节点」的位置。

如果不存在交点:两者会在走完 之后同时变为 ,退出循环。

import java.util.*;
public class Solution {
    public ListNode FindFirstCommonNode(ListNode a, ListNode b) {
        ListNode ta = a, tb = b;
        while (ta != tb) {
            ta = ta == null ? b : ta.next;
            tb = tb == null ? a : tb.next;
        }
        return ta;
    }
}

时间复杂度:O(n+m)
空间复杂度:O(1)

(7)《剑指offer》55 找到链表中环的入口结点【快慢指针】

(1)题目描述:
给一个链表,若其中包含环,请找出该链表的环的入口结点,否则,输出null。

例如,输入{1,2},{3,4,5}时,对应的环形链表如下图所示:
在这里插入图片描述可以看到环的入口结点的结点值为3,所以返回结点值为3的结点。

(2)解法一:快慢指针

本题是一个比较典型的链表题目,难度适中。首先,对于大多人来说,看到这道题是比较开心的,因为判断一个链表是否存在环的方法,基本上大家都知道,就是快慢指针法,但是再仔细一看,本题除了判断是否有环之外,还要找到这个环的入口点,这就略有些复杂了。

具体思路如下:

第一步:确定一个链表是否有环。这一步就是快慢指针法,定义两个指针,同时从链表的头结点出发,快指针一次走两步,慢指针一次走一步。如若有环,两个指针必定相遇,也就是如果快指针反追上了慢指针,说明存在环(这里要注意,两指针相遇的地方一定在环中,但不一定是环的入口),如果快指针走到了链表的末尾(指向了NULL),则说明不存在环。

第二步:找到环的入口点。这还是可以利用双指针来解决,两个指针初始都指向头结点,如果我们可以知道环中的结点个数,假设为n,那么第一个指针先向前走n步,然后两个指针(另一个从头结点开始)同时向前,当两个指针再次相遇时,他们的相遇点正好就是环的入口点。

这其实并不难理解,假设链表中共有m个结点,环中有n个结点,那么除环以外的结点数就是m-n,第一个指针先走了n步,然后两个指针一起向前,当他们一起向前m-n步时,第一个链表正好走完一遍链表,返回到环的入口,而另一个指针走了m-n步,也正好是到了环的入口。

现在,我们还有一个关键的问题:如何知道链表中的环包含了几个结点呢?也就是,怎么求这个n。

实际上这也不难,在第一步中,我们提到:快慢指针相遇的地方一定在环中,并且通过第一步我们已经找到了这个位置,接下来,只要从这个相遇的结点出发,一边移动一边计数,当它绕着环走一圈,再次回到这个结点时,就可以得到环中的结点数目n了。
在这里插入图片描述

public class Solution {

    public ListNode EntryNodeOfLoop(ListNode pHead) {
        //链表中环的入口结点
        if(pHead==null)
            return null;
        ListNode meetingNode=hasCycle(pHead);
        if(meetingNode==null)  //不存在环
            return null;
        
        ListNode head=pHead;
        while(head!=meetingNode){
            head=head.next;
            meetingNode=meetingNode.next;
        }
        return head;
    }

    public ListNode hasCycle(ListNode pHead){
        //判断是否有环,快慢指针法
        if(pHead==null || pHead.next==null)
            return null;
        ListNode slow=pHead,fast=pHead;
        while(fast!=null && fast.next!=null){
            slow=slow.next;
            fast=fast.next.next;
            if(slow==fast)
                return slow;
        }
        return null;
    }
}

(3)另一种写法

  1. 这题我们可以采用双指针解法,一快一慢指针。快指针每次跑两个element,慢指针每次跑一个。如果存在一个圈,总有一天,快指针是能追上慢指针的。
  2. 如下图所示,我们先找到快慢指针相遇的点,p。我们再假设,环的入口在点q,从头节点到点q距离为A,q p两点间距离为B,p q两点间距离为C。
  3. 因为快指针是慢指针的两倍速,且他们在p点相遇,则我们可以得到等式 2(A+B) = A+B+C+B. (感谢评论区大佬们的改正,此处应为:如果环前面的链表很长,而环短,那么快指针进入环以后可能转了好几圈(假设为n圈)才和慢指针相遇。但无论如何,慢指针在进入环的第一圈的时候就会和快的相遇。等式应更正为 2(A+B)= A+ nB + (n-1)C)
  4. 由3的等式,我们可得,C = A。
  5. 这时,因为我们的slow指针已经在p,我们可以新建一个另外的指针,slow2,让他从头节点开始走,每次只走下一个,原slow指针继续保持原来的走法,和slow2同样,每次只走下一个。
  6. 我们期待着slow2和原slow指针的相遇,因为我们知道A=C,所以当他们相遇的点,一定是q了。
  7. 我们返回slow2或者slow任意一个节点即可,因为此刻他们指向的是同一个节点,即环的起始点,q。
public class Solution {

    public ListNode EntryNodeOfLoop(ListNode pHead)
    {
        if(pHead == null || pHead.next == null){
            return null;
        }

        ListNode fast = pHead;
        ListNode slow = pHead;

        while(fast != null && fast.next != null){
            fast = fast.next.next;
            slow = slow.next;
            if(fast == slow){
                ListNode slow2 = pHead;
                while(slow2 != slow){
                    slow2 = slow2.next;
                    slow = slow.next;
                }
                return slow2;
            }
        }
        return null;
    }
}

(8)《剑指offer》56 删除链表中连续重复的结点【哈希表】

(1)题目描述:
在一个排序的链表中,存在重复的结点,请删除该链表中重复的结点,重复的结点不保留,返回链表头指针。 例如,链表1->2->3->3->4->4->5 处理后为 1->2->5。
在这里插入图片描述

(2)解法一
关于链表的大多数题目还是比较简单的,本题也并不太难。

删除重复结点,也就是如果当前结点和下一个结点的值相同,那么就是重复的结点,都可以被删除,为了保证删除之后的链表的连通性,在删除之后,要把当前结点前面的结点和下一个没有重复的结点链接起来,为此,程序需要记录当前的最后一个不重复结点,即程序中的pre。重点在于:一定要确保当前链接到链表中的一定是不会再重复的结点,具体见代码实现。

关于第一个结点如果重复怎么办的问题,我们不用单独考虑,可以使用链表中一贯的做法,加一个头结点即可。

具体思路看代码比较直观,参考如下的代码实现。
在这里插入图片描述


public class Solution {
    public ListNode deleteDuplication(ListNode pHead)
    {
        if(pHead == null || pHead.next == null){
            return pHead;
        }
        ListNode head = new ListNode(0);
        head.next = pHead;
        ListNode pre = head;
        ListNode last = head.next;
        while(last != null){
            if(last.next != null && last.val == last.next.val){
                //找到最后一个相同点
                while(last.next != null && last.val == last.next.val){
                    last = last.next;
                }
                pre.next = last.next;
                last = last.next;//zy
            }else{
                pre = pre.next;
                last = last.next;
            }
        }
        return head.next;
    }
}

/*1. 首先添加一个头节点,以方便碰到第一个,第二个节点就相同的情况

2.设置 pre ,last 指针, pre指针指向当前确定不重复的那个节点,而last指针相当于工作指针,一直往后面搜索。*/

(3)解法二:直接比较删除(推荐使用)
这是一个升序链表,重复的节点都连在一起,我们就可以很轻易地比较到重复的节点,然后将所有的连续相同的节点都跳过,连接不相同的第一个节点。

//遇到相邻两个节点值相同
if(cur.next.val == cur.next.next.val){ 
    int temp = cur.next.val;
    //将所有相同的都跳过
    while (cur.next != null && cur.next.val == temp) 
        cur.next = cur.next.next;
}

step 1:给链表前加上表头,方便可能的话删除第一个节点。

ListNode res = new ListNode(0);
//在链表前加一个表头
res.next = pHead;

step 2:遍历链表,每次比较相邻两个节点,如果遇到了两个相邻节点相同,则新开内循环将这一段所有的相同都遍历过去。
step 3:在step 2中这一连串相同的节点前的节点直接连上后续第一个不相同值的节点。
step 4:返回时去掉添加的表头。

public class Solution {
    public ListNode deleteDuplication(ListNode pHead) {
        //空链表
        if(pHead == null) 
            return null;
        ListNode res = new ListNode(0);
        //在链表前加一个表头
        res.next = pHead; 
        ListNode cur = res;
        while(cur.next != null && cur.next.next != null){ 
            //遇到相邻两个节点值相同
            if(cur.next.val == cur.next.next.val){ 
                int temp = cur.next.val;
                //将所有相同的都跳过
                while (cur.next != null && cur.next.val == temp) 
                    cur.next = cur.next.next;
            }
            else 
                cur = cur.next;
        }
        //返回时去掉表头
        return res.next; 
    }
}

时间复杂度:O(n),其中nnn为链表节点数,只有一次遍历
空间复杂度:O(1),只开辟了临时指针,常数级空间

(4)解法三:哈希表
这道题幸运的是链表有序,我们可以直接与旁边的元素比较,然后删除重复。那我们扩展一点,万一遇到的链表无序呢?我们这里给出一种通用的解法,有序无序都可以使用,即利用哈希表来统计是否重复。

step 1:遍历一次链表用哈希表记录每个节点值出现的次数。
step 2:在链表前加一个节点值为0的表头,方便可能的话删除表头元素。
step 3:再次遍历该链表,对于每个节点值检查哈希表中的计数,只留下计数为1的,其他情况都删除。
step 4:返回时去掉增加的表头。

import java.util.*;
public class Solution {
    public ListNode deleteDuplication(ListNode pHead) {
        //空链表
        if(pHead == null) 
            return null;
        Map<Integer,Integer> mp = new HashMap<>();
        ListNode cur = pHead;
        //遍历链表统计每个节点值出现的次数
        while(cur != null){ 
            if(mp.containsKey(cur.val))
                mp.put(cur.val, (int)mp.get(cur.val) + 1);
            else
                mp.put(cur.val,1);
            cur = cur.next;
        }
        ListNode res = new ListNode(0);
        //在链表前加一个表头
        res.next = pHead; 
        cur = res;
        //再次遍历链表
        while(cur.next != null){
            //如果节点值计数不为1 
            if(mp.get(cur.next.val) != 1) 
                //删去该节点
                cur.next = cur.next.next; 
            else
                cur = cur.next; 
        }
        //去掉表头
        return res.next; 
    }
}

时间复杂度:O(n),其中n为链表节点数,一共两次遍历,哈希表每次计数、每次查询都是O(1)
空间复杂度:O(n),最坏情况下n个节点都不相同,哈希表长度为n

(9)删除链表的节点

(1)题目描述:
给定单向链表的头指针和一个要删除的节点的值,定义一个函数删除该节点。返回删除后的链表的头节点。
在这里插入图片描述

(2)解法

public ListNode deleteNode (ListNode head, int val) {
    ListNode dummy = new ListNode(-1);
    dummy.next = head;
    ListNode node = dummy;
    while(node.next!=null){
        if(node.next.val==val){
           node.next = node.next.next;
            break;
        }
        node = node.next;
    }
    return dummy.next;
}

(四)树和二叉树

(1)《剑指offer》4-重建二叉树【递归、哈希表、栈】

(1)题目描述:
输入某二叉树的前序遍历和中序遍历的结果,请重建出该二叉树。假设输入的前序遍历和中序遍历的结果中都不含重复的数字。例如输入前序遍历序列{1,2,4,7,3,5,6,8}和中序遍历序列{4,7,2,1,5,3,8,6},则重建二叉树并返回。

(2)解题思路:
树的遍历有三种:分别是前序遍历、中序遍历、后序遍历。本题是根据前序和中序遍历序列重建二叉树,我们可以通过一个具体的实例来发现规律,不难发现:前序遍历序列的第一个数字就是树的根结点。在中序遍历序列中,可以扫描找到根结点的值,则左子树的结点都位于根结点的左边,右子树的结点都位于根结点的右边。

这样,我们就通过这两个序列找到了树的根结点、左子树结点和右子树结点,接下来左右子树的构建可以进一步通过递归来实现。

在这里插入图片描述
(3)解法一:递归构建二叉树
构建过程:
1-根据前序序列第一个结点确定根结点
2-根据根结点在中序序列中的位置分割出左右两个子序列
3-对左子树和右子树分别递归使用同样的方法继续分解

例如:
前序序列{1,2,4,7,3,5,6,8} = pre
中序序列{4,7,2,1,5,3,8,6} = in
在这里插入图片描述

1-根据当前前序序列的第一个结点确定根结点,为 1
2-找到 1 在中序遍历序列中的位置,为 in[3]
3-切割左右子树,则 in[3] 前面的为左子树, in[3] 后面的为右子树
4-则切割后的左子树前序序列为:{2,4,7},切割后的左子树中序序列为:{4,7,2};切割后的右子树前序序列为:{3,5,6,8},切割后的右子树中序序列为:{5,3,8,6}
5-对子树分别使用同样的方法分解

/**
 * Definition for binary tree
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode(int x) { val = x; }
 * }
 */
import java.util.Arrays;
public class Solution {
    public TreeNode reConstructBinaryTree(int [] pre,int [] in) {
        if (pre.length == 0 || in.length == 0) {
            return null;
        }
        TreeNode root = new TreeNode(pre[0]);
        // 在中序中找到前序的根
        for (int i = 0; i < in.length; i++) {
            if (in[i] == pre[0]) {
                // 左子树,注意 copyOfRange 函数,左闭右开
                root.left = reConstructBinaryTree(Arrays.copyOfRange(pre, 1, i + 1), Arrays.copyOfRange(in, 0, i));
                // 右子树,注意 copyOfRange 函数,左闭右开
                root.right = reConstructBinaryTree(Arrays.copyOfRange(pre, i + 1, pre.length), Arrays.copyOfRange(in, i + 1, in.length));
                break;
            }
        }
        return root;
    }
}

时间复杂度:O(n),其中n为数组长度,即二叉树的节点数,构建每个节点进一次递归,递归中所有的循环加起来一共n次
空间复杂度:O(n),递归栈最大深度不超过n,辅助数组长度也不超过n,重建的二叉树空间属于必要空间,不属于辅助空间

(3)解法二:借助哈希表

class Solution {
    int[] preorder;
    HashMap<Integer,Integer> hashMap = new HashMap<>();
    public TreeNode buildTree(int[] preorder, int[] inorder) {
        this.preorder = preorder;                  // 构建数组存储前序遍历的所有结点,方便找到根节点
        for(int i=0;i<inorder.length;++i) {     // 构建HashMap来存储中序遍历的所有结点,方便更快的找到根节点
            hashMap.put(inorder[i],i);
        }
        TreeNode node = recur(0,0,inorder.length-1);    // 0代表左遍历边界,inorder.length - 1代表右遍历边界
        return node;
    }
    public TreeNode recur(int root,int left,int right) { //root是根节点的下标,left是左遍历边界的下标,right是右遍历边界的下标
        if(left > right) return null;                    //递归终止条件
        TreeNode node = new TreeNode(preorder[root]);    //建立根节点
        int index = hashMap.get(preorder[root]);         //index是根节点在中序遍历的下标
        node.left = recur(root+1,left,index-1);          //递归左子树
        node.right = recur(root+index-left+1,index+1,right); //递归右子树,root+index-left+1是根节点索引 + 左子树长度 + 1即为右子树的根节点
        return node;
    }
}

(4)解法三:栈
除了递归,我们也可以用类似非递归前序遍历的方式建立二叉树,利用栈辅助进行非递归,然后依次建立节点。

step 1:首先前序遍历第一个数字依然是根节点,并建立栈辅助遍历。
step 2:然后我们就开始判断,在前序遍历中相邻的两个数字必定是只有两种情况:要么前序后一个是前一个的左节点;要么前序后一个是前一个的右节点或者其祖先的右节点。
step 3:我们可以同时顺序遍历pre和vin两个序列,判断是否是左节点,如果是左节点则不断向左深入,用栈记录祖先,如果不是需要弹出栈回到相应的祖先,然后进入右子树,整个过程类似非递归前序遍历。

import java.util.*;
public class Solution {
    public TreeNode reConstructBinaryTree(int [] pre,int [] vin) {
        int n = pre.length;
        int m = vin.length;
        //每个遍历都不能为0
        if(n == 0 || m == 0) 
            return null;
        Stack<TreeNode> s = new Stack<TreeNode>();
        //首先建立前序第一个即根节点
        TreeNode root = new TreeNode(pre[0]); 
        TreeNode cur = root;
        for(int i = 1, j = 0; i < n; i++){
            //要么旁边这个是它的左节点
            if(cur.val != vin[j]){ 
                cur.left = new TreeNode(pre[i]);
                s.push(cur);
                //要么旁边这个是它的右节点,或者祖先的右节点
                cur = cur.left; 
            }else{
                j++;
                //弹出到符合的祖先
                while(!s.isEmpty() && s.peek().val == vin[j]){
                    cur = s.pop();
                    j++;
                }
                //添加右节点
                cur.right = new TreeNode(pre[i]); 
                cur = cur.right;
            }
        }
        return root;
    }
}

时间复杂度:O(n),其中n为数组长度,即二叉树的节点数,遍历一次数组,弹出栈的循环最多进行n次
空间复杂度:O(n),栈空间最大深度为n,重建的二叉树空间属于必要空间,不属于辅助空间

(5)解法四

package tree;
 
public class ConstructTree{
	public Tree construct(int[] pre,int[] in){
		if(pre.length ==0 || in.length == 0)
			return null;
		Tree head = buildTree(pre,0,pre.length-1,in,0,in.length-1);
		return head;
	}
    public Tree buildTree(int[] preOrder, int begin1, int end1, int[] inOrder, int begin2, int end2) {
	// TODO Auto-generated method stub
    	if(begin1>end1||begin2>end2){
    		return null;
    	}
    	int rootData = preOrder[begin1];//前序的第一个数字总是树的根节点包含的数值
    	Tree head = new Tree(rootData);//初始化树的根节点
    	int divider = findIndexInArray(inOrder,rootData,begin2,end2);//找到根节点的值在中序遍历数组中的位置(索引下标)
    	int offSet = divider -begin2 -1;//在中序遍历数组中,左子树最后一个结点的下标
    	
    	Tree left = buildTree(preOrder,begin1+1,begin1+1+offSet,inOrder,begin2,begin2+offSet);
    	Tree right = buildTree(preOrder,begin1+offSet+2,end1,inOrder,divider+1,end2);
    	head.left_child = left;
    	head.right_child = right;
    	
    	return head;
    }
	private int findIndexInArray(int[] a, int x, int begin, int end) {
		// TODO Auto-generated method stub
		for(int i = begin;i<=end;i++){
			if(a[i]==x)
				return i;
		}
		return -1;
	}
	public void inOrder(Tree n){
		if(n!=null){
			inOrder(n.left_child);
		    System.out.print(n.val+" ");
		    inOrder(n.right_child);
		}
	}
	public void preOrder(Tree n){
		if(n!=null){
			System.out.print(n.val+" ");
			preOrder(n.left_child);
			preOrder(n.right_child);
		}
	}
	public static void main(String[] args){
		ConstructTree build =new ConstructTree();
		int[] preOrder = {1,2,4,7,3,5,6,8};
		int[] inOrder = {4,7,2,1,5,3,8,6};
		Tree root = build.construct(preOrder, inOrder);
		
		build.preOrder(root);//检验构建的二叉树前序遍历是否正确
		System.out.println();
		build.inOrder(root);//检验构建的二叉树中序遍历是否正确
	}	
}

(2)《剑指offer》17-树的子结构【递归、队列】

(1)题目描述:
输入两棵二叉树A,B,判断B是不是A的子结构。(ps:我们约定空树不是任意一个树的子结构)

(2)解题思路:
要查找树A中是否存在和树B结构一样的子树,我们可以分为两步:第一步,在树A中找到和树B的根结点值一样的结点R;第二步,判断树A中以R为根结点的子树是不是包含和树B一样的结构。
对于这两步,第一步实际上就是树的遍历,第二步是判断是否有相同的结构,这两步都可以通过递归来实现。
在这里插入图片描述
(3)解法一:两层前序遍历
既然是要找到A树中是否有B树这样子树,如果是有子树肯定是要遍历这个子树和B树,将两个的节点一一比较,但是这样的子树不一定就是A树根节点开始的,所以我们还要先找到子树可能出现的位置。

既然是可能的位置,那我们可以对A树的每个节点前序递归遍历,寻找是否有这样的子树,而寻找是否有子树的时候,我们就将A树与B树同步前序遍历,依次比较节点值。

//递归比较
bool flag1 = recursion(pRoot1, pRoot2);  
//递归树1的每个节点
bool flag2 = HasSubtree(pRoot1->left, pRoot2);  
bool flag3 = HasSubtree(pRoot1->right, pRoot2);

step 1:因为空树不是任何树的子树,所以要先判断B树是否为空树。
step 2:当A树为空节点,但是B树还有节点的时候,不为子树,但是A树不为空节点,B树为空节点时可以是子树。
step 3:每次递归比较A树从当前节点开始,是否与B树完全一致,同步前序遍历。
step 4:A树自己再前序遍历进入子节点,当作子树起点再与B树同步遍历。
step 5:以上情况任意只要有一种即可。

public class Solution {
    private boolean recursion(TreeNode root1, TreeNode root2){
        //当一个节点存在另一个不存在时
        if(root1 == null && root2 != null)  
            return false;
        //两个都为空则返回
        if(root1 == null || root2 == null)  
            return true;
        //比较节点值
        if(root1.val != root2.val)
            return false;
        //递归比较子树
        return recursion(root1.left, root2.left) && recursion(root1.right, root2.right);
    }

    public boolean HasSubtree(TreeNode root1, TreeNode root2) {
        //空树
        if(root2 == null) 
            return false;
        //一个为空,另一个不为空
        if(root1 == null && root2 != null)
            return false;
        if(root1 == null || root2 == null)
            return true;
        //递归比较
        boolean flag1 = recursion(root1, root2);  
        //递归树1的每个节点
        boolean flag2 = HasSubtree(root1.left, root2);  
        boolean flag3 = HasSubtree(root1.right, root2);
        return flag1 || flag2 || flag3;
    }
}

时间复杂度:O(nm),n和m分别表示两棵树的节点数,我们要对每个A树节点进行访问,最坏情况下每次都要比较B树节点的次数
空间复杂度:O(n+m),两个递归栈深度相乘(当树退化成链表时,递归栈最大)

(4)解法二:两层层次遍历(队列)
根据方法一,所以这道题的思路,无非就是在A树中遍历每个节点尝试找到那个子树,然后每次以该节点出发能不能将子树与B树完全匹配。能用前序遍历解决,我们也可以用层次遍历来解决。

首先对于A树层次遍历每一个节点,遇到一个与B树根节点相同的节点,我们就开始同步层次遍历比较以这个节点为根的树中是否出现了B树的全部节点。因为我们只考虑B树的所有节点是否在A树中全部出现,那我们就以B树为基,再进行一次层次遍历,A树从那个节点开始跟随B树一致进行层次遍历就行了,比较对应的每个点是否相同,或者B树是否有超出A树没有的节点。

//以树2为基础,树1跟随就可以了
while(!q2.isEmpty()){ 
    ...
    //树1为空或者二者不相等
    if(node1 == null || node1.val != node2.val) 
        return false;
    ...//继续层次遍历
}

层次遍历我们借助队列,根节点入队,然后取值:

q1.push(root1);
q2.push(root2);
while(...){
    TreeNode* node1 = q1.front(); 
    TreeNode* node2 = q2.front();
    ...
}

然后左右节点如果有就入队,每次队列弹出一个节点,知道队列为空,这样刚好是一层一层读取数据。

//树2还有左子树
if(node2->left){ 
    //子树入队
    q1.push(node1->left); 
    q2.push(node2->left);
}
//树2还有右子树
if(node2->right){ 
    //子树入队
    q1.push(node1->right); 
    q2.push(node2->right);
}

step 1:先判断空树,空树不为子结构。
step 2:利用队列辅助,层次遍历第一棵树,每次检查遍历到的节点是否和第二棵树的根节点相同。
step 3:若是相同,可以以该节点为子树根节点,再次借助队列辅助,同步层次遍历这个子树与第二棵树,这个时候以第二棵树为基,只要找到第二棵树的全部节点,就算找到了子结构。

import java.util.*;
public class Solution {
    //层次遍历判断两个树是否相同
    private boolean helper(TreeNode root1, TreeNode root2){ 
        Queue<TreeNode> q1 = new LinkedList<TreeNode>();
        Queue<TreeNode> q2 = new LinkedList<TreeNode>();
        q1.offer(root1);
        q2.offer(root2);
        //以树2为基础,树1跟随就可以了
        while(!q2.isEmpty()){ 
            TreeNode node1 = q1.poll(); 
            TreeNode node2 = q2.poll();
            //树1为空或者二者不相等
            if(node1 == null || node1.val != node2.val) 
                return false;
            //树2还有左子树
            if(node2.left != null){ 
                //子树入队
                q1.offer(node1.left); 
                q2.offer(node2.left);
            }
            //树2还有右子树
            if(node2.right != null){ 
                //子树入队
                q1.offer(node1.right); 
                q2.offer(node2.right);
            }
        }
        return true;
    }

    public boolean HasSubtree(TreeNode root1, TreeNode root2) {
        //空树不为子结构
        if(root1 == null || root2 == null) 
            return false;
        Queue<TreeNode> q = new LinkedList<TreeNode>();
        q.offer(root1);
        //层次遍历树1
        while(!q.isEmpty()){ 
            TreeNode node = q.poll();
            //遇到与树2根相同的节点,以这个节点为根判断后续子树是否相同
            if(node.val == root2.val){ 
                if(helper(node, root2))
                    return true;
            }
            //左子树入队
            if(node.left != null) 
                q.offer(node.left);
            //右子树入队
            if(node.right != null) 
                q.offer(node.right);
        }
        return false;
    }
}

时间复杂度:O(nm),n和m分别表示A树和B树的节点数,我们要对每个A树节点进行访问,最坏情况下每次都要比较B树节点的次数
空间复杂度:O(n),三个队列的长度最坏都不会超过A树的节点数

(5)另一种写法

public class Solution {
    public boolean HasSubtree(TreeNode root1,TreeNode root2) {

    if (root1 == null || root2 == null) {
            return false;
        }
        return judgeSubTree(root1, root2) ||
               judgeSubTree(root1.left, root2) ||
               judgeSubTree(root1.right, root2);
    }

    private boolean judgeSubTree(TreeNode root1, TreeNode root2) {
        if (root2 == null) {
            return true;
        }
        if (root1 == null) {
            return false;
        }
        if (root1.val != root2.val) {
            return false;
        }
        return judgeSubTree(root1.left, root2.left) &&
               judgeSubTree(root1.right, root2.right);
    }
}

(3)《剑指offer》18-二叉树的镜像【递归遍历、栈、队列】

(1)题目描述:
操作给定的二叉树,将其变换为源二叉树的镜像。
在这里插入图片描述

镜像二叉树
在这里插入图片描述
在这里插入图片描述

(2)解题思路:

求一棵树的镜像的过程:先前序遍历这棵树的每个结点,如果遍历到的结点有子结点,就交换它的两个子结点。当交换完所有的非叶结点的左、右子结点后,就可以得到该树的镜像。

如下面的例子,先交换根节点的两个子结点之后,我们注意到值为10、6的结点的子结点仍然保持不变,因此我们还需要交换这两个结点的左右子结点。做完这两次交换之后,我们已经遍历完所有的非叶结点。此时变换之后的树刚好就是原始树的镜像。
在这里插入图片描述
(3)解法一:递归(推荐使用)
因为我们需要将二叉树镜像,意味着每个左右子树都会交换位置,如果我们从上到下对遍历到的节点交换位置,但是它们后面的节点无法跟着他们一起被交换,因此我们可以考虑自底向上对每两个相对位置的节点交换位置,这样往上各个子树也会被交换位置。

自底向上的遍历方式,我们可以采用后序递归的方法。

step 1:先深度最左端的节点,遇到空树返回,处理最左端的两个子节点交换位置。
step 2:然后进入右子树,继续按照先左后右再回中的方式访问。
step 3:再返回到父问题,交换父问题两个子节点的值。

import java.util.*;
public class Solution {
    public TreeNode Mirror (TreeNode pRoot) {
        //空树返回
        if(pRoot == null) 
            return null;
        //先递归子树
        TreeNode left = Mirror(pRoot.left);  
        TreeNode right = Mirror(pRoot.right);
        //交换
        pRoot.left = right; 
        pRoot.right = left;
        return pRoot;
    }
}

时间复杂度:O(n),其中n为二叉树的节点数,访问二叉树所有节点各一次
空间复杂度:O(n),最坏情况下,二叉树退化为链表,递归栈最大值为n

(4)解法二:栈
二叉树中能够用递归的,我们大多也可以用栈来实现。栈的访问是一种自顶向下的访问,因此我们需要在左右子节点入栈后直接交换,然后再访问后续栈中内容。
step 1:优先检查空树的情况。
step 2:使用栈辅助遍历二叉树,根节点先进栈。
step 3:遍历过程中每次弹出栈中一个元素,然后该节点左右节点分别入栈。
step 4:同时我们交换入栈两个子节点的值,因为子节点已经入栈了再交换,就不怕后续没有交换。

import java.util.*;
public class Solution {
    public TreeNode Mirror (TreeNode pRoot) {
        //空树
        if(pRoot == null)  
            return null;
        //辅助栈
        Stack<TreeNode> s = new Stack<TreeNode>(); 
        //根节点先进栈
        s.push(pRoot); 
        while (!s.isEmpty()){ 
            TreeNode node = s.pop();
            //左右节点入栈
            if(node.left != null) 
                s.push(node.left);
            if(node.right != null) 
                s.push(node.right);
            //交换左右
            TreeNode temp = node.left; 
            node.left = node.right;
            node.right = temp;
        }
        return pRoot;
    }
}

时间复杂度:O(n),其中n为二叉树的节点数,访问二叉树所有节点各一次
空间复杂度:O(n),最坏情况下,二叉树退化为链表,栈的最大空间为n

(5)解法三:队列

public TreeNode Mirror(TreeNode root) {
    //如果为空直接返回
    if (root == null)
        return null;
    //队列
    final Queue<TreeNode> queue = new LinkedList<>();
    //首先把根节点加入到队列中
    queue.add(root);
    while (!queue.isEmpty()) {
        //poll方法相当于移除队列头部的元素
        TreeNode node = queue.poll();
        //交换node节点的两个子节点
        TreeNode left = node.left;
        node.left = node.right;
        node.right = left;
        //如果当前节点的左子树不为空,就把左子树
        //节点加入到队列中
        if (node.left != null) {
            queue.add(node.left);
        }
        //如果当前节点的右子树不为空,就把右子树
        //节点加入到队列中
        if (node.right != null) {
            queue.add(node.right);
        }
    }
    return root;
}

(4)《剑指offer》22-从上往下打印二叉树【递归、层次遍历Set】

(1)题目描述:
从上往下打印出二叉树的每个节点,同层节点从左至右打印。

(2)解题思路:
本题实际上就是二叉树的层次遍历,深度遍历可以用递归或者栈,而层次遍历很明显应该使用队列。同样我们可以通过一个例子来分析得到规律:每次打印一个结点时,如果该结点有子结点,则将子结点放到队列的末尾,接下来取出队列的头重复前面的打印动作,直到队列中所有的结点都打印完毕。

在这里插入图片描述

(3)解法一:层次遍历(队列)
二叉树的层次遍历就是按照从上到下每行,然后每行中从左到右依次遍历,得到的二叉树的元素值。对于层次遍历,我们通常会使用队列来辅助:

因为队列是一种先进先出的数据结构,我们依照它的性质,如果从左到右访问完一行节点,并在访问的时候依次把它们的子节点加入队列,那么它们的子节点也是从左到右的次序,且排在本行节点的后面,因此队列中出现的顺序正好也是从左到右,正好符合层次遍历的特点。

step 1:首先判断二叉树是否为空,空树没有遍历结果。
step 2:建立辅助队列,根节点首先进入队列。不管层次怎么访问,根节点一定是第一个,那它肯定排在队伍的最前面。
step 3:每次遍历队首节点,如果它们有子节点,依次加入队列排队等待访问。

import java.util.ArrayList;
import java.util.Queue;
import java.util.LinkedList;
/**
public class TreeNode {
    int val = 0;
    TreeNode left = null;
    TreeNode right = null;

    public TreeNode(int val) {
        this.val = val;
    }
}
*/
public class Solution {
    public ArrayList<Integer> PrintFromTopToBottom(TreeNode root) {

        ArrayList<Integer> result = new ArrayList<Integer>();
        if(root == null)return result;
        Queue<TreeNode> queue = new LinkedList<TreeNode>();
        queue.offer(root);
        while(!queue.isEmpty()){
            TreeNode temp = queue.poll();
            result.add(temp.val);
            if(temp.left != null)queue.offer(temp.left);
            if(temp.right != null)queue.offer(temp.right);
        }
        return result;
    }
}

时间复杂度:O(n),其中n为二叉树的节点数,每个节点访问一次
空间复杂度:O(n),队列的空间为二叉树的一层的节点数,最坏情况二叉树的一层为O(n)级

另一种写法:

   public ArrayList<Integer> PrintFromTopToBottom(TreeNode root) {
        //思路:使用队列来实现
        ArrayList<Integer> list=new ArrayList<>();
        if(root==null)return list;
        Queue<TreeNode> queue=new LinkedList<>(); 		//定义一个队列
        queue.add(root);
        while(queue.size()!=0){
            TreeNode temp=queue.poll();					//把队头元素丢到list中
            list.add(temp.val);
            if(temp.left!=null) queue.add(temp.left);  //左孩子不空,左孩子进队列
            if(temp.right!=null) queue.add(temp.right);//右孩子不空,右孩子进队列          
        }
        return list;
    }

(4)解法二:递归
既然二叉树的前序、中序、后序遍历都可以轻松用递归实现,树型结构本来就是递归喜欢的形式,那我们的层次遍历是不是也可以尝试用递归来试试呢?

按行遍历的关键是每一行的深度对应了它输出在二维数组中的深度,即深度可以与二维数组的下标对应,那我们可以在递归的访问每个节点的时候记录深度:

void traverse(TreeNode root, int depth)

进入子节点则深度加1:

//递归左右时深度记得加1
traverse(root.left, depth + 1); 
traverse(root.right, depth + 1);

每个节点值放入二维数组相应行。

res[depth - 1].push_back(root->val);

终止条件: 遍历到了空节点,就不再继续,返回。
返回值: 将加入的输出数组中的结果往上返回。
本级任务: 处理按照上述思路处理非空节点,并进入该节点的子节点作为子问题。

step 1:首先判断二叉树是否为空,空树没有遍历结果。
step 2:使用递归进行层次遍历输出,每次递归记录当前二叉树的深度,每当遍历到一个节点,如果为空直接返回。
step 3:如果遍历的节点不为空,输出二维数组中一维数组的个数(即代表了输出的行数)小于深度,说明这个节点应该是新的一层,我们在二维数组中增加一个一维数组,然后再加入二叉树元素。
step 4:如果不是step 3的情况说明这个深度我们已经有了数组,直接根据深度作为下标取出数组,将元素加在最后就可以了。
step 5:处理完这个节点,再依次递归进入左右节点,同时深度增加。因为我们进入递归的时候是先左后右,那么遍历的时候也是先左后右,正好是层次遍历的顺序。
step 6:最后将二维数组中的结果依次送入一维数组。

import java.util.*;
public class Solution {
    void traverse(TreeNode root, ArrayList<ArrayList<Integer> > res, int depth) {
        if(root != null){
            //新的一层
            if(res.size() < depth){  
                ArrayList<Integer> row = new ArrayList(); 
                res.add(row);
                row.add(root.val);
            //读取该层的一维数组,将元素加入末尾
            }else{ 
                ArrayList<Integer> row = res.get(depth - 1);
                row.add(root.val);
            }
        }
        else
            return;
        //递归左右时深度记得加1
        traverse(root.left, res, depth + 1); 
        traverse(root.right, res, depth + 1);
    }
    public ArrayList<Integer> PrintFromTopToBottom(TreeNode root) {
        ArrayList<Integer> res = new ArrayList();
        ArrayList<ArrayList<Integer> > temp = new ArrayList<ArrayList<Integer> >();
        if(root == null)
            //如果是空,则直接返回
            return res; 
        //递归层次遍历 
        traverse(root, temp, 1); 
        //送入一维数组
        for(int i = 0; i < temp.size(); i++)
            for(int j = 0; j < temp.get(i).size(); j++)
                res.add(temp.get(i).get(j));
        return res;
    }
}

(5)《剑指offer》23-二叉搜索树的后序遍历序列【递归、栈】

(1)题目描述:
输入一个整数数组,判断该数组是不是某二叉搜索树的后序遍历的结果。如果是则输出Yes,否则输出No。假设输入的数组的任意两个数字都互不相同。

(2)解法一:栈(推荐)
在这里插入图片描述在这里插入图片描述

import java.util.*;
public class Solution {
    public boolean VerifySquenceOfBST(int [] sequence) {
        // 处理序列为空情况
        if(sequence.length == 0) return false;
        Stack<Integer> s = new Stack<>();
        int root = Integer.MAX_VALUE;
        // 以根,右子树,左子树顺序遍历
        for(int i = sequence.length - 1; i >= 0; i--) {
            // 确定根后一定是在右子树节点都遍历完了,因此当前sequence未遍历的节点中只含左子树,左子树的节点如果>root则说明违背二叉搜索的性质
            if(sequence[i] > root) return false;
            // 进入左子树的契机就是sequence[i]的值小于前一项的时候,这时可以确定root
            while(!s.isEmpty() && s.peek() > sequence[i]) {
                root = s.pop();
            }
            // 每个数字都要进一次栈
            s.add(sequence[i]);
        }
        return true;
    }
}

时间复杂度:O(n),n代表sequence长度,各个节点均入栈、 出栈一次,因此时间代价和序列长度呈线性关系
空间复杂度:O(n),最差情况下单调栈存储所有节点,使用O(n)空间

(3)解法二:二叉树递归
递归函数的参数包含三个信息:遍历序列,树的起点索引位置,树的重点索引位置。

对于后序遍历的二叉搜索树来讲,终点位置就是根,然后从后往前遍历找到的第一个小于终点位置的节点,就是在序列中划分左右子树的分割位置。

只要能够一直划分下去,直到递归最后是单个节点,则说明应该返回true,否则返回false

step 1:首先对于给定列表长度为0的特殊情况返回false
step 2:递归函数中返回条件为 l>=r,返回true
step 3:递归函数中确定根节点为sequence[r],然后从后往前遍历找到左右子树分割点,进行继续递归

import java.util.*;
public class Solution {
    public boolean VerifySquenceOfBST(int [] sequence) {
        if(sequence.length == 0) return false;
        return order(sequence, 0, sequence.length - 1);
    }
    
    public boolean order(int [] sequence, int l, int r) {
        // 剩一个节点的时候 返回 true
        if(l >= r) return true;
        int j;
        int mid = sequence[r];
        
        // 找到左子树和右子树的分界点,j代表左子树的最后一个索引位置
        for(j = r; j >= l; j--) {
            int cur = sequence[j];
            if(cur < mid) break;
        }
        
        // 判断所谓的左子树中是否又不合法(不符合二叉搜索树)的元素
        for(int i = j; i >= l; i--) {
            int cur = sequence[i];
            if(cur > mid) return false;
        }
        return order(sequence, l, j) && order(sequence, j+1, r-1);
    }
}

在这里插入图片描述

(4)递归思路的另一种解释
对于后续遍历序列,序列的最后一个值一定是树的根结点,而由二叉搜索树的性质:左小右大,我们可以从头开始遍历,当遍历到某个值比根结点大时停止,记为flag,此时flag之前的所有数值都是二叉搜索树的左子树的结点,flag以及flag之后的所有数都是二叉搜索树的右子树的结点。这是由二叉搜索树以及后序遍历共同决定的。

接下来,我们就可以把任务交给递归,同样的方法去判断左子树和右子树是否是二叉搜索树,这显然是典型的递归解法。

举例:

以{5,7,6,9,11,10,8}为例,后序遍历结果的最后一个数字8就是根结点的值。在这个数组中,前3个数字5、7和6都比8小,是值为8的结点的左子树结点;后3个数字9、11和10都比8大,是值为8的结点的右子树结点。

我们接下来用同样的方法确定与数组每一部分对应的子树的结构。这其实就是一个递归的过程。对于序列5、7、6,最后一个数字6是左子树的根结点的值。数字5比6小,是值为6的结点的左子结点,而7则是它的右子结点。同样,在序列9、11、10中,最后一个数字10是右子树的根结点,数字9比10小,是值为10的结点的左子结点,而11则是它的右子结点,所以它对应的二叉搜索树如下:
在这里插入图片描述

public class Solution {
    public boolean VerifySquenceOfBST(int [] sequence) {
        if(sequence == null || sequence.length == 0)
            return false;
        return helpVerify(sequence, 0, sequence.length-1);
    }
    
    public boolean helpVerify(int [] sequence, int start, int root){
        if(start >= root)
            return true;
        int key = sequence[root];
        int i;
        //找到左右子数的分界点
        for(i=start; i < root; i++)
            if(sequence[i] > key)
                break;
        //在右子树中判断是否含有小于root的值,如果有返回false
        for(int j = i; j < root; j++)
            if(sequence[j] < key)
                return false;
        return helpVerify(sequence, start, i-1) && helpVerify(sequence, i, root-1);
    }
}

(6)《剑指offer》24-二叉树中和为某一值的路径【递归、深度优先遍历+栈】

(1)题目描述:
输入一颗二叉树的根节点和一个整数,打印出二叉树中结点值的和为输入整数的所有路径。路径定义为从树的根结点开始往下一直到叶结点所经过的结点形成一条路径。(注意: 在返回值的list中,数组长度大的数组靠前)

(2)解法一:非递归(深度遍历、栈)
本题实质上就是深度优先搜索DFS。使用前序遍历的方式对整棵树进行遍历,当访问到某一个结点时,将该结点添加到路径上,并且累加该结点的值。当访问到的结点是叶结点时,如果路径中的结点值之和正好等于输入的整数,则说明是一条符合要求的路径。如果当前结点不是叶结点,则继续访问它的子结点。

当找到一条符合要求的路径之后,需要回溯进一步查找别的路径,因此,这实际上仍然是一个递归的过程,特别注意在函数返回之前要删掉当前结点,从而才可以正确的回溯。
在这里插入图片描述

import java.util.ArrayList;
/**
public class TreeNode {
    int val = 0;
    TreeNode left = null;
    TreeNode right = null;

    public TreeNode(int val) {
        this.val = val;
    }
}
*/
public class Solution {
    private ArrayList<ArrayList<Integer>> result = new ArrayList<ArrayList<Integer>>();
    private ArrayList<Integer> list = new ArrayList<>();
    public ArrayList<ArrayList<Integer>> FindPath(TreeNode root,int target) {
        if(root == null)return result;
        list.add(root.val);
        target -= root.val;
        if(target == 0 && root.left == null && root.right == null)
            result.add(new ArrayList<Integer>(list));
        //因为在每一次的递归中,我们使用的是相同的result引用,所以其实左右子树递归得到的结果我们不需要关心,
        //可以简写为FindPath(root.left, target);FindPath(root.right, target);
        //但是为了大家能够看清楚递归的真相,此处我还是把递归的形式给大家展现了出来。
        ArrayList<ArrayList<Integer>> result1 = FindPath(root.left, target);
        ArrayList<ArrayList<Integer>> result2 = FindPath(root.right, target);
        list.remove(list.size()-1);
        return result;
    }
}

另一种写法:

public class Solution {
    //这个res集合用来存储符合题目条件的集合,(用集合来存储集合,所以是<ArrayList<Integer>>)
    private ArrayList<ArrayList<Integer>> res = new ArrayList<>();
    //lengthCompare是自己定义的类,在最下面,该类继承了Comparator<ArrayList>接口,重写了其中的compare方法,用来比较两个ArrayList的长度大小,作用是让长度长的集合排在前面
    private lengthCompare c=new lengthCompare();
    
    //找路径外面的方法
    public ArrayList<ArrayList<Integer>> FindPath(TreeNode root,int target) {
        if(root==null) return res;
        ArrayList<Integer> temp=new ArrayList<>();	//temp集合用来存储算法所爬过的结点,这些结点不一定符合
        FindPath(root,target,temp);	//这个函数处理完之后,在res集合中添加了很多符合条件的集合,下面就对这些集合进行排序
        res.sort(c);				//让长度大的集合排在前面
        return res;					//最后返回排序完后所有符合条件的集合
    }
    
     //找路径里面的核心方法
    public void FindPath(TreeNode root,int target,ArrayList<Integer> temp){
        temp.add(root.val);
        if(root.left==null && root.right==null){	 //root是叶结点
            if(root.val==target) {					//找到了一条路径,则下面把该路径存储到res集合当中
                ArrayList<Integer> list=new ArrayList();	//list集合用来存储算法所爬过的结点,这些结点符合题目条件,最后添加到res中
                list.addAll(temp);
                res.add(list);
            }
        }
        else{
            if(root.left!=null)		//如果左孩子结点非空,则递归循环左孩子结点
                FindPath(root.left,target-root.val,temp);
            if(root.right!=null)	//如果右孩子结点非空,则递归循环左孩子结点
                FindPath(root.right,target-root.val,temp);
        }
        
        //回溯
        if(temp.size()!=0) 
            temp.remove(temp.size()-1);
    }

//lengthCompare是自己定义的类,在最下面,该类继承了Comparator<ArrayList>接口,重写了其中的compare方法,用来比较两个ArrayList的长度大小,作用是让长度长的集合排在前面
    class  lengthCompare implements Comparator<ArrayList>{
        public int compare(ArrayList a,ArrayList b){
            if(a.size()>b.size())
               return -1;
            else if(a.size()==b.size())
                return 0;
            else
                return 1;
        }
    }
}

(3)解法二:
既然是检查从根到叶子有没有一条等于目标值的路径,那肯定需要从根节点遍历到叶子,我们可以在根节点每次往下一层的时候,将sum减去节点值,最后检查是否完整等于0. 而遍历的方法我们可以选取二叉树常用的递归前序遍历,因为每次进入一个子节点,更新sum值以后,相当于对子树查找有没有等于新目标值的路径,因此这就是子问题,递归的三段式为:

终止条件: 每当遇到节点为空,意味着过了叶子节点,返回。每当检查到某个节点没有子节点,它就是叶子节点,此时sum减去叶子节点值刚好为0,说明找到了路径。
返回值: 将子问题中是否有符合新目标值的路径层层往上返回。
本级任务: 每一级需要检查是否到了叶子节点,如果没有则递归地进入子节点,同时更新sum值减掉本层的节点值。

step 1:每次检查遍历到的节点是否为空节点,空节点就没有路径。
step 2:再检查遍历到是否为叶子节点,且当前sum值等于节点值,说明可以刚好找到。
step 3:检查左右子节点是否可以有完成路径的,如果任意一条路径可以都返回true,因此这里选用两个子节点递归的或。

import java.util.*;
public class Solution {
    public boolean hasPathSum (TreeNode root, int sum) {
        //空节点找不到路径
        if(root == null) 
            return false;
        //叶子节点,且路径和为sum
        if(root.left == null && root.right == null && sum - root.val == 0) 
            return true;
        //递归进入子节点
        return hasPathSum(root.left, sum - root.val) || hasPathSum(root.right, sum - root.val); 
    }
}

时间复杂度:O(n),其中n为二叉树所有节点,前序遍历二叉树所有节点
空间复杂度:O(n),最坏情况二叉树化为链表,递归栈空间最大为n

(7)《剑指offer》26-二叉搜索树与双向链表【中序遍历、递归、栈】

(1)题目描述:
输入一棵二叉搜索树,将该二叉搜索树转换成一个排序的双向链表。要求不能创建任何新的结点,只能调整树中结点指针的指向。
在这里插入图片描述在这里插入图片描述

(2)解法一:递归(中序遍历)
首先要理解此题目的含义,在双向链表中,每个结点都有前后两个指针;二叉树中,每个结点都有两个指向子结点的左右指针,同时,二叉搜索树树也是一种排序的数据结构。因此,从结构上看,双向链表的前后指针和二叉搜索树的左右指针结构相似,因此,可以实现互相之间的转换。

首先,根据二叉搜索树的特点,左结点的值<根结点的值<右结点的值,据此不难发现,使用二叉树的中序遍历得到的数据序列就是递增的排序顺序。因此,首先确定应该采用中序遍历方法。

接下来,可以根据下图,将树分为三个部分,值为10的根结点、根为6的左子树和根为14的右子树。不难看出以下规律:根据中序遍历的顺序,当我们遍历到根结点时,它的左子树已经转换为一个排好序的双向链表,并且链表最后一个结点是左子树值最大的结点,我们把这个值最大(8)的结点同根结点链接起来,10就成了最后一个结点,接着遍历右子树,将根结点同右子树中最小的结点链接起来。
在这里插入图片描述step 1:创建两个指针,一个指向题目中要求的链表头(head),一个指向当前遍历的前一节点(pre)。
step 2:首先递归到最左,初始化head与pre。
step 3:然后处理中间根节点,依次连接pre与当前节点,连接后更新pre为当前节点。
step 4:最后递归进入右子树,继续处理。
step 5:递归出口即是节点为空则返回。

public class Solution {
    //返回的第一个指针,即为最小值,先定为null
    public TreeNode head = null;  
    //中序遍历当前值的上一位,初值为最小值,先定为null
    public TreeNode pre = null;   
    public TreeNode Convert(TreeNode pRootOfTree) {
        if(pRootOfTree == null)
            //中序递归,叶子为空则返回
            return null;     
        //首先递归到最左最小值  
        Convert(pRootOfTree.left); 
        //找到最小值,初始化head与pre
        if(pre == null){       
            head = pRootOfTree;
            pre = pRootOfTree;
        }
        //当前节点与上一节点建立连接,将pre设置为当前值
        else{       
            pre.right = pRootOfTree;
            pRootOfTree.left = pre;
            pre = pRootOfTree;
        }
        Convert(pRootOfTree.right);
        return head;
    }
}

时间复杂度:O(n),其中n为二叉树节点数,中序遍历所有节点
空间复杂度:O(n),递归栈所需要的最大空间

另一种写法:

/**
public class TreeNode {
    int val = 0;
    TreeNode left = null;
    TreeNode right = null;

    public TreeNode(int val) {
        this.val = val;

    }

}
*/
import java.util.ArrayList;
public class Solution {
    public TreeNode Convert(TreeNode pRootOfTree) {
        if(pRootOfTree == null){
            return null;
        }
        ArrayList<TreeNode> list = new ArrayList<>();
        Convert(pRootOfTree, list);
        return Convert(list);
    }
    //中序遍历,在list中按遍历顺序保存
    public void Convert(TreeNode pRootOfTree, ArrayList<TreeNode> list){
        if(pRootOfTree.left != null){
            Convert(pRootOfTree.left, list);
        }
 
        list.add(pRootOfTree);
 
        if(pRootOfTree.right != null){
            Convert(pRootOfTree.right, list);
        }
    }
    //遍历list,修改指针
    public TreeNode Convert(ArrayList<TreeNode> list){
        for(int i = 0; i < list.size() - 1; i++){
            list.get(i).right = list.get(i + 1);
            list.get(i + 1).left = list.get(i);
        }
        return list.get(0);
    }
}

思路分析:中序遍历二叉树,然后用一个ArrayList类保存遍历的结果,这样在ArratList中节点就按顺序保存了,然后再来修改指针

(3)解法二:非递归中序遍历(栈)
二叉树中序遍历除了递归方法,我们还可以尝试非递归解法,与常规的非递归中序遍历几乎相同,还是借助栈来辅助,只是增加了连接节点。

step 1:创建两个指针,一个指向题目中要求的链表头(head),一个指向当前遍历的前一节点(pre),创建一个布尔型变量,标记是否是第一次到最左,因为第一次到最左就是链表头。
step 2:判断空树不能连接。
step 3:初始化一个栈辅助中序遍历。
step 4:依次将父节点加入栈中,直接进入二叉树最左端。
step 5:第一次进入最左,初始化head与pre,然后进入它的根节点开始连接。
step 6:最后将右子树加入栈中,栈中依次就弹出“左中右”的节点顺序,直到栈为空。

import java.util.*;
public class Solution {
    public TreeNode Convert(TreeNode pRootOfTree) {
        if (pRootOfTree == null)
            return null;
        //设置栈用于遍历
        Stack<TreeNode> s = new Stack<TreeNode>(); 
        TreeNode head = null;
        TreeNode pre = null;
        //确认第一个遍历到最左,即为首位
        boolean isFirst = true; 
        while(pRootOfTree != null || !s.isEmpty()){
            //直到没有左节点
            while(pRootOfTree != null){  
                s.push(pRootOfTree);
                pRootOfTree = pRootOfTree.left;
            }
            pRootOfTree = s.pop();
            //最左元素即表头
            if(isFirst){  
                head = pRootOfTree;
                pre = head;
                isFirst = false;
            //当前节点与上一节点建立连接,将pre设置为当前值
            }else{  
                pre.right = pRootOfTree;
                pRootOfTree.left = pre;
                pre = pRootOfTree;
            }
            pRootOfTree = pRootOfTree.right;
        }
        return head;
    }
}

时间复杂度:O(n),其中n为二叉树节点数,中序遍历二叉树所有节点
空间复杂度:O(n),栈s最大空间为为O(n)

(8)《剑指offer》38计算二叉树的深度【递归、层次遍历】

(1)题目描述:
输入一棵二叉树,求该树的深度。从根结点到叶结点依次经过的结点(含根、叶结点)形成树的一条路径,最长路径的长度为树的深度。

(2)解法一:递归
本题相对比较简单。根据二叉树深度的定义,我们有以下理解:如果一棵树只有一个结点,那么它的深度为1。如果根结点只有左子树而没有右子树,那么树的深度为其左子树深度加1;相反,如果根结点只有右子树而没有左子树,那么深度为右子树深度加1;如果既有左子树又有右子树,那么该树的深度为左、右子树深度的较大值加1。

因此,很明显本题应该使用递归的思路来解决。
在这里插入图片描述
在这里插入图片描述

class Solution {
    public int maxDepth(TreeNode root) {
        if(root == null) return 0;
        return Math.max(maxDepth(root.left), maxDepth(root.right)) + 1; //为什么要加1,是因为1代表根节点的深度
    }
}

时间复杂度:O(n),其中n为二叉树的节点数,遍历整棵二叉树
空间复杂度:O(n),最坏情况下,二叉树化为链表,递归栈深度最大为n

(3)解法二:层次遍历(队列)
既然是统计二叉树的最大深度,除了根据路径到达从根节点到达最远的叶子节点以外,我们还可以分层统计。对于一棵二叉树而言,必然是一层一层的,那一层就是一个深度,有的层可能会很多节点,有的层如根节点或者最远的叶子节点,只有一个节点,但是不管多少个节点,它们都是一层。因此我们可以使用层次遍历,二叉树的层次遍历就是从上到下按层遍历,每层从左到右,我们只要每层统计层数即是深度。

step 1:既然是层次遍历,我们遍历完一层要怎么进入下一层,可以用队列记录这一层中节点的子节点。队列类似栈,只不过是一个先进先出的数据结构,可以理解为我们平时的食堂打饭的排队。因为每层都是按照从左到右开始访问的,那自然记录的子节点也是从左到右,那我们从队列出来的时候也是从左到右,完美契合。
step 2:在刚刚进入某一层的时候,队列中的元素个数就是当前层的节点数。比如第一层,根节点先入队,队列中只有一个节点,对应第一层只有一个节点,第一层访问结束后,它的子节点刚好都加入了队列,此时队列中的元素个数就是下一层的节点数。因此遍历的时候,每层开始统计该层个数,然后遍历相应节点数,精准进入下一层。
step 3:遍历完一层就可以节点深度就可以加1,直到遍历结束,即可得到最大深度。

import java.util.*;
public class Solution {
    public int maxDepth (TreeNode root) {
        //空节点没有深度
        if(root == null) 
            return 0;
        //队列维护层次后续节点
        Queue<TreeNode> q = new LinkedList<TreeNode>(); 
        //根入队
        q.offer(root); 
        //记录深度
        int res = 0; 
        //层次遍历
        while(!q.isEmpty()){ 
            //记录当前层有多少节点
            int n = q.size(); 
            //遍历完这一层,再进入下一层
            for(int i = 0; i < n; i++){ 
                TreeNode node = q.poll();
                //添加下一层的左右节点
                if(node.left != null) 
                    q.offer(node.left);
                if(node.right != null)
                    q.offer(node.right);
            }
            //深度加1
            res++; 
        }
        return res; 
    }
}

时间复杂度:O(n),其中nnn为二叉树的节点数,遍历整棵二叉树
空间复杂度:O(n),辅助队列的空间最坏为n

(9)《剑指offer》39 判断是否为平衡二叉树【DFS、回溯】

(1)题目描述:
输入一棵二叉树,判断该二叉树是否是平衡二叉树。这里的定义是:如果某二叉树中任意结点的左、右子树的深度相差不超过1,那么它就是一棵平衡二叉树。
在这里插入图片描述

(2)解法一:dfs
首先对于本题我们要正确理解,一般情况下,平衡二叉树就是AVL树,它首先是二叉搜索树(左小右大),其次满足左右子树高度之差不超过1。但是在本题中,没有二叉搜索树的要求,只对平衡与否进行判断即可。

根据求二叉树深度的思路我们很容易想到一种解法,即:在遍历树的每一个结点时,求其左右子树的深度,判断深度之差,如果每个结点的左右子树深度相差都不超过1,那么就是一棵平衡二叉树。本思路直观简洁,但是需有很多结点需要重复遍历多次,时间效率不高。

为了避免重复遍历,我们可以得到一种 每个结点只遍历一次的解法。 思路如下:采用后序遍历的方式遍历二叉树的每个结点,这样在遍历到每个结点的时候就已经访问了它的左右子树。所以,只要在遍历每个结点的时候记录它的深度,我们就可以一边遍历一边判断每个结点是不是平衡的。

如果一个节点的左右子节点都是平衡的,并且左右子节点的深度差不超过 1,则可以确定这个节点就是一颗平衡二叉树。

 public boolean isBalanced(TreeNode root) {
    if(root == null)return true;
    return Math.abs(recur(root.left) - recur(root.right)) <= 1 && isBalanced(root.left) && isBalanced(root.right);  //先序遍历,先判断当前根节点是否满足条件,再判断其左右子树是否满足条件
}
public int recur(TreeNode root) {
    if(root==null) return 0;
    return Math.max(recur(root.left),recur(root.right)) + 1;
}

在这里插入图片描述

(3)解法二:回溯
对于父节点,需要确定两个子节点深度之差小于一。对于作为子节点的立场,需要向自己的上一级节点传递的自己深度

public class Solution {
    public boolean IsBalanced_Solution(TreeNode root) {
        if(deep(root)==-1) return false;
        return true;   
    }
     public int deep(TreeNode node){
        if(node==null) return 0;
        int left=deep(node.left);
        if(left == -1 ) return -1;
        int right=deep(node.right);
        if(right == -1 ) return -1;

        //两个子节点深度之差小于一
        if((left-right)>1 || (right-left)>1){
            return -1;
        }
        //父节点需要向自己的父节点报告自己的深度
        return (left>right?left:right)+1;
    }
}

时间复杂度:O(n) 其中 n 是所有节点个数
空间复杂度:O(n) 主要是递归方***占用本地方法栈,而递归层数不会超过n次

(10)《剑指offer》39 给定一个二叉树和其中的一个结点,请找出中序遍历顺序的下一个结点并且返回。【递归】

(1)题目描述:
给定一个二叉树和其中的一个结点,请找出中序遍历顺序的下一个结点并且返回。注意,树中的结点不仅包含左右子结点,同时包含指向父结点的指针。

(2)解法一:中序遍历递归(推荐使用)
我们首先要根据给定输入中的结点指针向父级进行迭代,直到找到该树的根节点;然后根据根节点进行中序遍历,当遍历到和给定树节点相同的节点时,下一个节点就是我们的目标返回节点

step 1:首先先根据当前给出的结点找到根节点
step 2:然后根节点调用中序遍历
step 3:将中序遍历结果存储下来
step 4:最终拿当前结点匹配是否有符合要求的下一个结点

import java.util.*;
public class Solution {
    ArrayList<TreeLinkNode> nodes = new ArrayList<>();
    public TreeLinkNode GetNext(TreeLinkNode pNode) {
        // 获取根节点
        TreeLinkNode root = pNode;
        while(root.next != null) root = root.next;
        
        // 中序遍历打造nodes
        InOrder(root);
        
        // 进行匹配
        int n = nodes.size();
        for(int i = 0; i < n - 1; i++) {
            TreeLinkNode cur = nodes.get(i);
            if(pNode == cur) {
                return nodes.get(i+1);
            }
        }
        return null;
    }
    
    // 中序遍历
    void InOrder(TreeLinkNode root) {
        if(root != null) {
            InOrder(root.left);
            nodes.add(root);
            InOrder(root.right);
        }
    }
}

时间复杂度:O(N),因为遍历了树中的所有节点
空间复杂度:O(N),因为引入了存储所有节点的空间

(3)解法二:分类直接寻找
直接寻找分为三种情况

1-如果给出的结点有右子节点,则最终要返回的下一个结点即右子树的最左下的结点
2-如果给出的结点无右子节点,且当前结点是其父节点的左子节点,则返回其父节点
3-如果给出的结点无右子节点,且当前结点是其父节点的右子节点,则先要沿着左上方父节点爬树,一直爬到当前结点是其父节点的左子节点为止,返回的就是这个父节点;或者没有满足上述情况的则返回为NULL

step 1:判断该节点是否符合思路中第一点,则一直找到右子树的左下节点为返回值
step 2:判断该节点是否符合思路中第二点,则返回当前节点的父亲节点
step 3:判断该节点是否符合思路中第三点,则迭代向上找父节点,直到迭代的当前节点是父节点的左孩子节点为止,返回该父节点;如果不满足上述情况返回NULL

import java.util.*;
public class Solution {
    public TreeLinkNode GetNext(TreeLinkNode pNode) {
        // 情况一
        if(pNode.right != null) {
            TreeLinkNode rchild = pNode.right;
            // 一直找到右子树的最左下的结点为返回值
            while(rchild.left != null) rchild = rchild.left; 
            return rchild;
        }
        
        // 情况二
        if(pNode.next != null && pNode.next.left == pNode) {
            return pNode.next;
        }
        
        // 情况三
        if(pNode.next != null) {
            TreeLinkNode ppar = pNode.next;
            // 沿着左上一直爬树,爬到当前结点是其父节点的左自己结点为止
            while(ppar.next != null && ppar.next.right == ppar) ppar = ppar.next; 
            return ppar.next;
        }
        return null;
    }
}

时间复杂度:O(N),最大代价是当树退化成一个只包含右子节点的链表,当给定节点是中序遍历最后一个节点时,会进入情况三的分析部分,在向左上方向一直迭代直到根节点,才会发现应该返回NULL,即无下一个节点,此时代价最大。
空间复杂度:O(1),无额外空间的借用

(2)解题思路:
本题解决起来并不是很困难,主要是分析清楚所有可能的不同情况。对于中序遍历序列来说,遵循“左->根->右”的顺序,在深刻理解中序遍历的基础上,结合一些具体的实例我们不难得出以下结论。

找一个结点在中序遍历序列中的下一个结点共有三种不同的情况:

如果一个结点有右子树,那么它的下一个结点就是它的右子树中最左边的那个结点,也就是说,从它的右子结点出发一直访问左指针,最后就可以找到这个最左结点。
如果一个结点没有右子树,那么需要再进一步考虑,不难知道:如果这个结点是其父结点的左结点,那么根据中序遍历规则,它的下一个结点就是它的父结点。
第三种情况略复杂一些,当一个结点既没有右子树,也不是其父结点的左结点时,我们可以沿着指向父结点的指针一直向上遍历,直到找到一个是它自身的父结点的左孩子的结点,如果这样的结点存在,那么这个结点的父结点就是我们要找的下一个结点。

在这里插入图片描述

以上图中的树为例,其中序遍历序列是:d,b,h,e,i,a,f,c,g。

对应第一种情况(有右子树的情形),例如,图中结点b的下一个结点是h,结点a的下一个结点是f。

对应第二种情况(没有右子树,但是其父结点的左结点的情形),例如,图中结点d的下一个结点是b,f的下一个结点是c。

对应第三种情况(没有右子树,但是是其父结点的右结点),例如,为了找到结点g的下一个结点,我们沿着指向父结点的指针向上遍历,先到达结点c。由于结点c是父结点a的右结点,我们继续向上遍历到达结点a。由于结点a是树的根结点。它没有父结点。因此结点g没有下一个结点。

    public boolean isBalanced(TreeNode root) {
        if(root == null)return true;   //这里一共有两个递归,第一个递归的终止条件就是roor==null
        return Math.abs(recur(root.left) - recur(root.right)) <= 1 && isBalanced(root.left) && isBalanced(root.right);  //这里相当于先序遍历,Math.abs相当于先序遍历里面的输出语句,而后面的就是递归左右子树。他的整个执行顺序应该就是先递归左右子树,然后再让Math.abs判断子树的深度
    }
    public int recur(TreeNode root) {
        if(root==null) return 0;        //输出长度的递归终止条件一律是return 0,输出boolean的条件一律是return null
        return Math.max(recur(root.left),recur(root.right)) + 1;  //+1应该是加上根节点
    }

(11)《剑指offer》58 判断一棵二叉树是不是对称的【递归、层次遍历+队列】

(1)题目描述:
请实现一个函数,用来判断一颗二叉树是不是对称的。注意,如果一个二叉树同此二叉树的镜像是同样的,定义其为对称的。

下面这棵二叉树是对称的
在这里插入图片描述下面这棵二叉树不对称。
在这里插入图片描述

(2)解题思路:
本题判断一棵树是不是对称的,和第18题可以对比分析:二叉树的镜像,和LeetCode第101题:101. 对称二叉树是同一道题。

(3)解法一:递归法
前序遍历的时候我们采用的是“根左右”的遍历次序,如果这棵二叉树是对称的,即相应的左右节点交换位置完全没有问题,那我们是不是可以尝试“根右左”遍历,按照轴对称图像的性质,这两种次序的遍历结果应该是一样的。

不同的方式遍历两次,将结果拿出来比较看起来是一种可行的方法,但也仅仅可行,太过于麻烦。我们不如在遍历的过程就结果比较了。而遍历方式依据前序递归可以使用递归:

1-终止条件: 当进入子问题的两个节点都为空,说明都到了叶子节点,且是同步的,因此结束本次子问题,返回true;当进入子问题的两个节点只有一个为空,或是元素值不相等,说明这里的对称不匹配,同样结束本次子问题,返回false。
2-返回值: 每一级将子问题是否匹配的结果往上传递。
3-本级任务: 每个子问题,需要按照上述思路,“根左右”走左边的时候“根右左”走右边,“根左右”走右边的时候“根右左”走左边,一起进入子问题,需要两边都是匹配才能对称。

step 1:两种方向的前序遍历,同步过程中的当前两个节点,同为空,属于对称的范畴。
step 2:当前两个节点只有一个为空或者节点值不相等,已经不是对称的二叉树了。
step 3:第一个节点的左子树与第二个节点的右子树同步递归对比,第一个节点的右子树与第二个节点的左子树同步递归比较。

public class Solution {
    boolean recursion(TreeNode root1, TreeNode root2){
        //可以两个都为空
        if(root1 == null && root2 == null) 
            return true;
        //只有一个为空或者节点值不同,必定不对称
        if(root1 == null || root2 == null || root1.val != root2.val) 
            return false;
        //每层对应的节点进入递归比较
        return recursion(root1.left, root2.right) && recursion(root1.right, root2.left);
    }
    boolean isSymmetrical(TreeNode pRoot) {
        return recursion(pRoot, pRoot);
    }
}

时间复杂度:O(n),其中n为二叉树的节点数,相当于遍历整个二叉树两次
空间复杂度:O(n),最坏情况二叉树退化为链表,递归栈深度为n

(4)解法二:迭代法(广度优先遍历+队列)

广度优先遍历的一般做法是借助队列,这里我们可以在初始化时把根节点入队两次。每次提取两个结点并比较它们的值(队列中每两个连续的结点应该是相等的,而且它们的子树互为镜像),然后将两个结点的左右子结点按相反的顺序插入队列中。当队列为空时,或者我们检测到树不对称(即从队列中取出两个不相等的连续结点)时,该算法结束。

这一方法的关键是队列中出队列的两个连续的结点就应当是对称树中相等的结点。
在这里插入图片描述

public boolean isSymmetric(TreeNode root) {
    if(root == null)return true;    // 如果根节点为空,那肯定是对称
    return recur(root.left,root.right);   //好,接下来我就要判断这个根节点的左右子树是否都是对称了
}
public boolean recur(TreeNode L,TreeNode R) {
    if(L == null && R == null) return true;   //正常来说,这个返回true的判断条件应该是L.left == R.right && L.right == R.left,但是他要一直递归下去,因此最后的终止条件是到达叶子结点,也就是L == null,R == null
    if(L == null || R == null || L.val != R.val) return false;  //当左右子树有一个已经到达叶子结点没有了,已经null了,或者值不相等那就是false
    return recur(L.left,R.right) && recur(L.right,R.left);  //记住,树的题大多数递归,递归就要记住两个点:终止条件+递归条件
}

(5)解法三:层次遍历(队列)
除了递归以外,我们还可以观察,对称的二叉树每一层都是回文的情况,即两边相互对应相等,有节点值的对应节点值,没有节点的连空节点都是对应着的呢。那我们从左往右遍历一层(包括空节点),和从右往左遍历一层(包括空节点),是不是就是得到一样的结果了。(注:必须包含空节点,因为空节点乱插入会导致不同,如题干第二个图所示)。

这时候二叉树每一层的遍历,我就需要用到了层次遍历。层次遍历从左往右经过第一层后,怎么进入第二层?我们可以借助队列——一个先进先出的容器,在遍历第一层的时候,将第一层节点的左右节点都加入到队列中,因为加入队列的顺序是遍历的顺序且先左后右,也就导致了我从队列出来的时候也是下一层的先左后右,正好一一对应。更巧的是,如果我们要从右到左遍历一层,加入队列后也是先右后左,简直完美对应!

//从左往右加入队列
q1.offer(left.left); 
q1.offer(left.right);
//从右往左加入队列
q2.offer(right.right); 
q2.offer(right.left);

而且我们不需要两个层次遍历都完整地遍历二叉树,只需要一半就行了,从左往右遍历左子树,从右往左遍历右子树,各自遍历一半相互比对,因为遍历到另一半都已经检查过了。

step 1:首先判断链表是否为空,空链表直接就是对称。
step 2:准备两个队列,分别作为从左往右层次遍历和从右往左层次遍历的辅助容器,初始第一个队列加入左节点,第二个队列加入右节点。
step 3:循环中每次从队列分别取出一个节点,如果都为空,暂时可以说是对称的,进入下一轮检查;如果某一个为空或是两个节点值不同,那必定不对称。其他情况暂时对称,可以依次从左往右加入子节点到第一个队列,从右往左加入子节点到第二个队列。(这里包括空节点)
step 4:遍历结束也没有检查到不匹配,说明就是对称的。
请添加图片描述

import java.util.*;
public class Solution {
    boolean isSymmetrical(TreeNode pRoot) {
        //空树为对称的
        if(pRoot == null) 
            return true;
        //辅助队列用于从两边层次遍历
        Queue<TreeNode> q1 = new LinkedList<TreeNode>(); 
        Queue<TreeNode> q2 = new LinkedList<TreeNode>();
        q1.offer(pRoot.left);
        q2.offer(pRoot.right);
        while(!q1.isEmpty() && !q2.isEmpty()){ 
            //分别从左边和右边弹出节点
            TreeNode left = q1.poll(); 
            TreeNode right = q2.poll();
            //都为空暂时对称
            if(left == null && right == null)
                continue;
            //某一个为空或者数字不相等则不对称
            if(left == null || right == null || left.val != right.val)
                return false;
            //从左往右加入队列
            q1.offer(left.left); 
            q1.offer(left.right);
            //从右往左加入队列
            q2.offer(right.right); 
            q2.offer(right.left);
        }
        //都检验完都是对称的
        return true;
    }
}

时间复杂度:O(n),其中n为二叉树的节点个数,相当于遍历二叉树全部节点
空间复杂度:O(n),两个辅助队列的最大空间为n

(12)《剑指offer》59 按照之字型打印二叉树【层次遍历+队列、栈】

(1)题目描述:
请实现一个函数按照之字形打印二叉树,即第一行按照从左到右的顺序打印,第二层按照从右至左的顺序打印,第三行按照从左到右的顺序打印,其他行以此类推。

(2)解法一:层次遍历+队列(推荐)
按照层次遍历按层打印二叉树的方式,每层分开打印,然后对于每一层利用flag标记,第一层为false,之后每到一层取反一次,如果该层的flag为true,则记录的数组整个反转即可。

//奇数行反转,偶数行不反转
if(flag) 
    reverse(row.begin(), row.end());

但是难点在于如何每层分开存储,从哪里知晓分开的时机?在层次遍历的时候,我们通常会借助队列(queue),事实上,队列中的值大有玄机,让我们一起来看看:当根节点进入队列时,队列长度为1,第一层节点数也为1;若是根节点有两个子节点,push进队列后,队列长度为2,第二层节点数也为2;若是根节点一个子节点,push进队列后,队列长度为为1,第二层节点数也为1。由此,我们可知,每层的节点数等于进入该层时队列长度,因为刚进入该层时,这一层每个节点都会push进队列,而上一层的节点都出去了。

int n = temp.size();
for(int i = 0; i < n; i++){
    //访问一层
}

step 1:首先判断二叉树是否为空,空树没有打印结果。
step 2:建立辅助队列,根节点首先进入队列。不管层次怎么访问,根节点一定是第一个,那它肯定排在队伍的最前面,初始化flag变量。
step 3:每次进入一层,统计队列中元素的个数,更改flag变量的值。因为每当访问完一层,下一层作为这一层的子节点,一定都加入队列,而再下一层还没有加入,因此此时队列中的元素个数就是这一层的元素个数。
step 4:每次遍历这一层这么多的节点数,将其依次从队列中弹出,然后加入这一行的一维数组中,如果它们有子节点,依次加入队列排队等待访问。
step 5:访问完这一层的元素后,根据flag变量决定将这个一维数组直接加入二维数组中还是反转后再加入,然后再访问下一层。

import java.util.*;
public class Solution {
    public ArrayList<ArrayList<Integer> > Print(TreeNode pRoot) {
        TreeNode head = pRoot;
        ArrayList<ArrayList<Integer> > res = new ArrayList<ArrayList<Integer>>();
        if(head == null)
            //如果是空,则直接返回空list
            return res; 
        //队列存储,进行层次遍历
        Queue<TreeNode> temp = new LinkedList<TreeNode>(); 
        temp.offer(head);
        TreeNode p;
        boolean flag = true;
        while(!temp.isEmpty()){
            //记录二叉树的某一行
            ArrayList<Integer> row = new ArrayList<Integer>();  
            int n = temp.size();
            //奇数行反转,偶数行不反转
            flag = !flag; 
            //因先进入的是根节点,故每层节点多少,队列大小就是多少
            for(int i = 0; i < n; i++){
                p = temp.poll();
                row.add(p.val);
                //若是左右孩子存在,则存入左右孩子作为下一个层次
                if(p.left != null)
                    temp.offer(p.left);
                if(p.right != null)
                    temp.offer(p.right);
            }
            //奇数行反转,偶数行不反转
            if(flag)  
                Collections.reverse(row);
            res.add(row);
        }
        return res;
    }
}

时间复杂度:O(n),每个节点访问一次,因为reverse的时间复杂度为O(n),按每层元素reverse也相当于O(n)
空间复杂度:O(n),队列的空间最长为O(n)

(3)解法二:双栈法
方法一用到了反转函数,反转我们能想到什么?肯定是先进后出的栈!

我们可以利用两个栈遍历这棵二叉树,第一个栈s1从根节点开始记录第一层,然后依次遍历两个栈,遍历第一个栈时遇到的子节点依次加入第二个栈s2中,即是第二层:

//遍历奇数层
while(!s1.empty()){ 
    TreeNode* node = s1.top();
    //记录奇数层
    temp.push_back(node->val); 
    //奇数层的子节点加入偶数层
    if(node->left)  
        s2.push(node->left);
    if(node->right) 
        s2.push(node->right);
    s1.pop();
}

而遍历第二个栈s2的时候因为是先进后出,因此就是逆序的,再将第二个栈s2的子节点依次加入第一个栈s1中:

while(!s2.empty()){ 
    reeNode* node = s2.top();
    //记录偶数层
    temp.push_back(node->val);  
    //偶数层的子节点加入奇数层
    if(node->right)  
        s1.push(node->right);
    if(node->left) 
        s1.push(node->left);
    s2.pop();
}

于是原本的逆序在第一个栈s1中又变回了正序,如果反复交替直到两个栈都空为止。

step 1:首先判断二叉树是否为空,空树没有打印结果。
step 2:建立两个辅助栈,每次依次访问第一个栈s1与第二个栈s2,根节点先进入s1.
step 3:依据依次访问的次序,s1必定记录的是奇数层,访问节点后,将它的子节点(如果有)依据先左后右的顺序加入s2,这样s2在访问的时候根据栈的先进后出原理就是右节点先访问,正好是偶数层需要的从右到左访问次序。偶数层则正好相反,要将子节点(如果有)依据先右后左的顺序加入s1,这样在s1访问的时候根据栈的先进后出原理就是左节点先访问,正好是奇数层需要的从左到右访问次序。
step 4:每次访问完一层,即一个栈为空,则将一维数组加入二维数组中,并清空以便下一层用来记录。

import java.util.*;
public class Solution {
    public ArrayList<ArrayList<Integer> > Print(TreeNode pRoot) {
        TreeNode head = pRoot;
        ArrayList<ArrayList<Integer> > res = new ArrayList<ArrayList<Integer>>();
        if(head == null)
            //如果是空,则直接返回空list
            return res; 
        Stack<TreeNode> s1 = new Stack<TreeNode>();
        Stack<TreeNode> s2 = new Stack<TreeNode>();
        //放入第一次
        s1.push(head); 
        while(!s1.isEmpty() || !s2.isEmpty()){ 
            ArrayList<Integer> temp = new ArrayList<Integer>();
            //遍历奇数层
            while(!s1.isEmpty()){ 
                TreeNode node = s1.pop();
                //记录奇数层
                temp.add(node.val); 
                //奇数层的子节点加入偶数层
                if(node.left != null)  
                    s2.push(node.left);
                if(node.right != null) 
                    s2.push(node.right);
            }
            //数组不为空才添加
            if(temp.size() != 0)  
                res.add(new ArrayList<Integer>(temp));
            //清空本层数据
            temp.clear(); 
            //遍历偶数层
            while(!s2.isEmpty()){ 
                TreeNode node = s2.pop();
                //记录偶数层
                temp.add(node.val);  
                //偶数层的子节点加入奇数层
                if(node.right != null)  
                    s1.push(node.right);
                if(node.left != null) 
                    s1.push(node.left);
            }
            //数组不为空才添加
            if(temp.size() != 0) 
                res.add(new ArrayList<Integer>(temp));
            //清空本层数据
            temp.clear(); 
        }
        return res;
    }

}

时间复杂度:O(n),其中n为二叉树的节点数,遍历二叉树的每个节点
空间复杂度:O(n),两个栈的空间最坏情况为n

(4)解法三:栈

这道题仍然是二叉树的遍历,相当于层次遍历,可以和第22题:从上往下打印二叉树 和第60题:把二叉树打印成多行 这几个题对比起来进行分析。

相对而言,本题按之字形顺序打印二叉树是比较复杂的,短时间内不太好分析得到结论,可以通过具体的实例来进行分析,从具体的例子得出普遍的结论。
在这里插入图片描述实际上,层次遍历我们都是借助一定的数据容器来实现的,比如按行打印使用的是队列。在本题,我们使用的是栈,具体分析如下:我们可以设置两个辅助栈,在打印某一层的结点时,将下一层的子结点保存到相应的栈里;如果当前打印的是奇数层(第一层、第三层等),则先保存左子节点再保存右子结点到第一个栈中,如果当前打印的是偶数层(第二层、第四层等),则先保存右子结点再保存左子结点到第二个栈中。
在这里插入图片描述

import java.util.ArrayList;
import java.util.Stack;
public class TreeNode {
    int val = 0;
    TreeNode left = null;
    TreeNode right = null;

    public TreeNode(int val) {
        this.val = val;
    }
}
public class Solution {
    public ArrayList<ArrayList<Integer> > Print(TreeNode pRoot) {
        /*
        思路:之字形打印,用两个栈来实现
        打印奇数行时,将他的左右节点保存到另一个栈中,先左后右
        打印偶数行时,同样将左右节点保存到栈中,先右后左
        */
        ArrayList<ArrayList<Integer>> res=new ArrayList<>();	//result用来存储结果
        if(pRoot==null)return res;								//判空,直接返回result结果,即为空
        
        Stack[] stack=new Stack[2]; //stack[0]保存偶数层,stack[1]保存奇数层,注意java不支持泛型数组
        stack[0]=new Stack();		
        stack[1]=new Stack();
        
        TreeNode root=pRoot;		//根节点
        stack[1].push(root);		//根节点先放在保存奇数层的栈stack[1]
        int num=1; 		//当前打印的是第几层
        
        while((!stack[0].isEmpty())||(!stack[1].isEmpty())){ //有一个栈不为空
            int flag=num%2; //当前要打印的栈
            ArrayList<Integer> row=new ArrayList<>();		//row存取每一行的结点
            while(!stack[flag].isEmpty()){
                TreeNode temp=(TreeNode)stack[flag].pop();
                if(flag==1) { //当前是奇数行,注意栈是先进后出,所以left和right反过来
                    if(temp.left!=null)
                        stack[0].push(temp.left);
                    if(temp.right!=null)
                        stack[0].push(temp.right);
                }else{ 		 //当前是偶数行
                    if(temp.right!=null)
                        stack[1].push(temp.right);
                    if(temp.left!=null)
                        stack[1].push(temp.left);
                }
                row.add(temp.val); //temp是每一个结点,每一个结点先存到行row集合中,最后row集合再存入到res结果集合中
            }
            res.add(row);		   //row集合再存入到res结果集合中
            num++;				   //遍历完一行就倒下一行
        } 
        return res;				   //最后返回结果结合res
     }

}

(5)解法四:BFS
1-主要的方法与BFS写法没什么区别
2-BFS里是每次只取一个,而我们这里先得到队列长度size,这个size就是这一层的节点个数,然后通过for循环去poll出这size个节点,这里和按行取值二叉树返回ArrayList<ArrayList>这种题型的解法一样,之字形取值的核心思路就是通过两个方法:

  • list.add(T): 按照索引顺序从小到大依次添加
  • list.add(index, T): 将元素插入index位置,index索引后的元素依次后移,这就完成了每一行元素的倒序,或者使用Collection.reverse()方法倒序也可以
import java.util.LinkedList;
public class Solution {
    public ArrayList<ArrayList<Integer> > Print(TreeNode pRoot) {
        LinkedList<TreeNode> q = new LinkedList<>();
        ArrayList<ArrayList<Integer>> res = new ArrayList<>();
        boolean rev = true;
        q.add(pRoot);
        while(!q.isEmpty()){
            int size = q.size();
            ArrayList<Integer> list = new ArrayList<>();
            for(int i=0; i<size; i++){
                TreeNode node = q.poll();
                if(node == null){continue;}
                if(rev){
                    list.add(node.val);
                }else{
                    list.add(0, node.val);
                }
                q.offer(node.left);
                q.offer(node.right);
            }
            if(list.size()!=0){res.add(list);}
            rev=!rev;
        }
        return res;
    }
}

(13)《剑指offer》60 把二叉树打印成多行

题目描述:

从上到下按层打印二叉树,同一层结点从左至右输出。每一层输出一行。

解题思路:

本题可类比第22题:从上往下打印二叉树,这两道题实际上是一回事,只不过这里我们多了一个分行打印的要求,实际上大同小异,稍加修改即可。

在二叉树层次遍历上,我们使用的是队列,借助队列先进先出的性质实现,具体规律:每次打印一个结点时,如果该结点有子结点,则将子结点放到队列的末尾,接下来取出队列的头重复前面的打印动作,直到队列中所有的结点都打印完毕。在此基础上我们考虑这里的分行要求,不难想到我们只要增加两个变量即可:一个用于保存当前层中还没有打印的结点个数,另一个用于记录下一层结点的数目。而使用队列的话,实际上这两个变量可以统一用队列的长度来实现。

在这里插入图片描述

import java.util.*;
public class TreeNode {
    int val = 0;
    TreeNode left = null;
    TreeNode right = null;

    public TreeNode(int val) {
        this.val = val;
    }
}
public class Solution {
    ArrayList<ArrayList<Integer> > Print(TreeNode pRoot) {
        //思路:使用队列实现
        ArrayList<ArrayList<Integer>> res=new ArrayList<>();
        if(pRoot==null)
            return res;
        Queue<TreeNode> queue = new LinkedList<>(); //借助队列实现
        TreeNode root=pRoot;
        queue.add(root);
        while(!queue.isEmpty()){ //队列不空
            //当前队列长度代表当前这一层节点个数
            int len=queue.size();
            ArrayList<Integer> row=new ArrayList<>();
            for(int i=0;i<len;i++){  //循环次数,也就是当前这一层节点个数
                 TreeNode temp=queue.poll();
                 if(temp.left!=null)
                     queue.add(temp.left);
                 if(temp.right!=null)
                     queue.add(temp.right);
                row.add(temp.val);
            }
           res.add(row);
        }
        return res;
    }
}

(14)《剑指offer》61 序列化二叉树

在这里插入图片描述在这里插入图片描述

public class TreeNode {
    int val = 0;
    TreeNode left = null;
    TreeNode right = null;

    public TreeNode(int val) {
        this.val = val;
    }
}
public class Solution {
	//序列化
    String Serialize(TreeNode root){
        if(root==null) return "#,";
        String res="";
        res+=root.val+",";   //前序遍历,根左右
        res+=Serialize(root.left);
        res+=Serialize(root.right);
        return res;
    }

    //反序列化
    int start=-1;
    TreeNode Deserialize(String str){
        if(str==null || str.length()==0)return null;
        String[] strArr=str.split(",");
        return Deserialize(strArr);
    }
    TreeNode Deserialize(String[] strArr){
        start++;
        if(start>=strArr.length || strArr[start].equals("#")) return null;
        TreeNode cur=new TreeNode(Integer.valueOf(strArr[start]));
        cur.left=Deserialize(strArr);
        cur.right=Deserialize(strArr);
        return cur;
    }
}

(15)《剑指offer》62 查找二叉搜索树的第k小的结点【中序遍历、栈】

(1)题目描述:
给定一棵二叉搜索树,请找出其中的第k小的结点。例如(5,3,7,2,4,6,8) 中,按结点数值大小顺序第三小结点的值为4。

(2)解法一:递归中序遍历
根据二叉搜索树的性质,左子树的元素都小于根节点,右子树的元素都大于根节点。因此它的中序遍历(左中右)序列正好是由小到大的次序,因此我们可以尝试递归中序遍历,也就是从最小的一个节点开始,找到k个就是我们要找的目标。

step 1:设置全局变量count记录遍历了多少个节点,res记录第k个节点。
step 2:另写一函数进行递归中序遍历,当节点为空或者超过k时,结束递归,返回。
step 3:优先访问左子树,再访问根节点,访问时统计数字,等于k则找到。
step 4:最后访问右子树。

import java.util.*;
public class Solution {
    //记录返回的节点
    private TreeNode res = null;
    //记录中序遍历了多少个
    private int count = 0;
    public void midOrder(TreeNode root, int k){
        //当遍历到节点为空或者超过k时,返回
        if(root == null || count > k) 
            return;
        midOrder(root.left, k);
        count++;
        //只记录第k个
        if(count == k)  
            res = root;
        midOrder(root.right, k);
    }
    public int KthNode (TreeNode proot, int k) {
        midOrder(proot, k);
        if(res != null)
            return res.val;
        //二叉树为空,或是找不到
        else 
            return -1;
    }
}

时间复杂度:O(n),其中n为二叉树的节点数,所有节点遍历一遍
空间复杂度:O(n),栈空间最大深度为二叉树退化为链表,长度为n

(3)解法二:递归中序遍历
本题实际上比较简单,主要还是考察对树的遍历的理解,只要熟练掌握了树的三种遍历方式及其特点,解决本题并不复杂,很明显本题是对中序遍历的应用。

对于本题,我们首先可以知道二叉搜索树的特点:左结点的值<根结点的值<右结点的值。因此,我们不难得到如下结论:如果按照中序遍历的顺序对一棵二叉搜索树进行遍历,那么得到的遍历序列就是递增排序的。因此,只要用中序遍历的顺序遍历一棵二叉搜索树,就很容易找出它的第k大结点。
在这里插入图片描述
上面的树中序遍历的结果:2 3 4 5 6 7 8

/*
public class TreeNode {
    int val = 0;
    TreeNode left = null;
    TreeNode right = null;
    public TreeNode(int val) {
        this.val = val;
    }
}
*/
public class Solution {
    int index = 0;//计数器;
    //必须得利用result节点 因为递归不断return 得有最终的一个变量保存最终结果
    TreeNode result = null;
    
    //思路:二叉搜索树按照中序遍历的顺序打印出来正好就是排序好的顺序。
    //所以,按照中序遍历顺序找到第k个结点就是结果。
    TreeNode KthNode(TreeNode pRoot, int k)
    {
        
        if(pRoot == null){
            return pRoot;
        }else{
            KthNode(pRoot.left, k);
       
            index++;
            if(index == k){
                result = pRoot;
            }
            KthNode(pRoot.right, k);
        }
        
       return result;
    }
}

(4)解法三:非递归中序遍历(栈)
递归实际上就是一种先进后出的栈结构,因此能用递归进行的中序遍历,非递归(栈)也可以实现,还是需要记录遍历到第k位即截止。

step 1:用栈记录当前节点,不断往左深入,直到左边子树为空。
step 2:再弹出栈顶(即为当前子树的父节点),访问该节点,同时计数。
step 3:然后再访问其右子树,其中每棵子树都遵循左中右的次序。
step 4:直到第k个节点返回,如果遍历结束也没找到,则返回-1.

import java.util.*;
public class Solution {
    public int KthNode (TreeNode proot, int k) {
        if(proot == null)
            return -1;
        //记录遍历了多少个数
        int count = 0;  
        TreeNode p = null;
        //用栈辅助建立中序
        Stack<TreeNode> s = new Stack<TreeNode>();   
        while(!s.isEmpty() || proot != null){
            while (proot != null) {
                s.push(proot);
                //中序遍历每棵子树从最左开始
                proot = proot.left;   
            }
            p = s.pop();
            count++;
            //第k个直接返回
            if(count == k) 
                return p.val;
            proot = p.right;
        }
        //没有找到
        return -1;
    }
}

时间复杂度:O(n),其中n为二叉树的节点数,所有节点遍历一遍
空间复杂度:O(n),栈空间最大值为二叉树退化为链表

(16)在二叉树中找到两个节点的最近公共祖先【递归、DFS、BFS】

(1)题目描述:
给定一棵二叉树(保证非空)以及这棵树上的两个节点对应的val值 o1 和 o2,请找到 o1 和 o2 的最近公共祖先节点。
在这里插入图片描述在这里插入图片描述
(2)解法一:非递归写法(BFS)
要想找到两个节点的最近公共祖先节点,我们可以从两个节点往上找,每个节点都往上走,一直走到根节点,那么根节点到这两个节点的连线肯定有相交的地方,如果是从上往下走,那么最后一次相交的节点就是他们的最近公共祖先节点。我们就以找6和7的最近公共节点来画个图看一下
在这里插入图片描述我们看到6和7公共祖先有5和3,但最近的是5。我们只要往上找,找到他们第一个相同的公共祖先节点即可,但怎么找到每个节点的父节点呢,我们只需要把每个节点都遍历一遍,然后顺便记录他们的父节点存储在Map中。我们先找到其中的一条路径,比如6→5→3,然后在另一个节点往上找,由于7不在那条路径上,我们找7的父节点是2,2也不在那条路径上,我们接着往上找,2的父节点是5,5在那条路径上,所以5就是他们的最近公共子节点。

其实这里我们可以优化一下,我们没必要遍历所有的结点,我们一层一层的遍历(也就是BFS),只需要这两个节点都遍历到就可以了,比如上面2和8的公共结点,我们只需要遍历到第3层,把2和8都遍历到就行了,没必要再遍历第4层了。(BFS就是一层一层的遍历,如下图所示)
在这里插入图片描述

public int lowestCommonAncestor(TreeNode root, int o1, int o2) {
    //记录遍历到的每个节点的父节点。
    Map<Integer, Integer> parent = new HashMap<>();
    Queue<TreeNode> queue = new LinkedList<>();
    parent.put(root.val, Integer.MIN_VALUE);//根节点没有父节点,给他默认一个值
    queue.add(root);
    //直到两个节点都找到为止。
    while (!parent.containsKey(o1) || !parent.containsKey(o2)) {
        //队列是一边进一边出,这里poll方法是出队,
        TreeNode node = queue.poll();
        if (node.left != null) {
            //左子节点不为空,记录下他的父节点
            parent.put(node.left.val, node.val);
            //左子节点不为空,把它加入到队列中
            queue.add(node.left);
        }
        //右节点同上
        if (node.right != null) {
            parent.put(node.right.val, node.val);
            queue.add(node.right);
        }
    }
    Set<Integer> ancestors = new HashSet<>();
    //记录下o1和他的祖先节点,从o1节点开始一直到根节点。
    while (parent.containsKey(o1)) {
        ancestors.add(o1);
        o1 = parent.get(o1);
    }
    //查看o1和他的祖先节点是否包含o2节点,如果不包含再看是否包含o2的父节点……
    while (!ancestors.contains(o2))
        o2 = parent.get(o2);
    return o2;
}

时间复杂度:O(n),n是二叉树节点的个数,最坏情况下每个节点都会被访问一遍
空间复杂度:O(n),一个是BFS需要的队列,一个是父子节点关系的map

(3)解法二:递归
step 1:如果o1和o2中的任一个和root匹配,那么root就是最近公共祖先。
step 2:如果都不匹配,则分别递归左、右子树。
step 3:如果有一个节点出现在左子树,并且另一个节点出现在右子树,则root就是最近公共祖先.
step 4:如果两个节点都出现在左子树,则说明最低公共祖先在左子树中,否则在右子树。
step 5:继续递归左、右子树,直到遇到step1或者step3的情况。

public int lowestCommonAncestor(TreeNode root, int o1, int o2) {
    return helper(root, o1, o2).val;
}

public TreeNode helper(TreeNode root, int o1, int o2) {
    if (root == null || root.val == o1 || root.val == o2)
        return root;
    TreeNode left = helper(root.left, o1, o2);
    TreeNode right = helper(root.right, o1, o2);
    //如果left为空,说明这两个节点在root结点的右子树上,我们只需要返回右子树查找的结果即可
    if (left == null)
        return right;
    //同上
    if (right == null)
        return left;
    //如果left和right都不为空,说明这两个节点一个在root的左子树上一个在root的右子树上,
    //我们只需要返回cur结点即可。
    return root;
}

时间复杂度:O(n),n是二叉树节点的个数,最坏情况下每个节点都会被访问一遍
空间复杂度:O(n),因为是递归,取决于栈的深度,最差最差情况下,二叉树退化成链表,栈的深度是n。

(4)解法三:深度优先搜索(DFS)
既然要找到二叉树中两个节点的最近公共祖先,那我们可以考虑先找到两个节点全部祖先,可以得到从根节点到目标节点的路径,然后依次比较路径得出谁是最近的祖先。

找到两个节点的所在可以深度优先搜索遍历二叉树所有节点进行查找。

step 1:利用dfs求得根节点到两个目标节点的路径:每次选择二叉树的一棵子树往下找,同时路径数组增加这个遍历的节点值。
step 2:一旦遍历到了叶子节点也没有,则回溯到父节点,寻找其他路径,回溯时要去掉数组中刚刚加入的元素。
step 3:然后遍历两条路径数组,依次比较元素值。
step 4:找到两条路径第一个不相同的节点即是最近公共祖先。

import java.util.*;
public class Solution {
    //记录是否找到到o的路径
    public boolean flag = false; 
    //求得根节点到目标节点的路径
    public void dfs(TreeNode root, ArrayList<Integer> path, int o){ 
        if(flag || root == null)
            return;
        path.add(root.val);
        //节点值都不同,可以直接用值比较
        if(root.val == o){ 
            flag = true;
            return;
        }
        //dfs遍历查找
        dfs(root.left, path, o); 
        dfs(root.right, path, o);
        //找到
        if(flag)
            return;
        //回溯
        path.remove(path.size() - 1);
    }
    public int lowestCommonAncestor (TreeNode root, int o1, int o2) {
        ArrayList<Integer> path1 = new ArrayList<Integer>(); 
        ArrayList<Integer> path2 = new ArrayList<Integer>(); 
        //求根节点到两个节点的路径
        dfs(root, path1, o1); 
        //重置flag,查找下一个
        flag = false; 
        dfs(root, path2, o2);
        int res = 0; 
        //比较两个路径,找到第一个不同的点
        for(int i = 0; i < path1.size() && i < path2.size(); i++){ 
            int x = path1.get(i);
            int y = path2.get(i);
            if(x == y) 
                //最后一个相同的节点就是最近公共祖先
                res = x; 
            else
                break;
        }
        return res;
    }
}

时间复杂度:O(n),其中n为二叉树节点数,递归遍历二叉树每一个节点求路径,后续又遍历路径
空间复杂度:O(n),最坏情况二叉树化为链表,深度为n,递归栈深度和路径数组为n

(17)二叉搜索树的最近公共祖先【递归】

(1)题目描述:
给定一个二叉搜索树, 找到该树中两个指定节点的最近公共祖先。
1.对于该题的最近的公共祖先定义:对于有根树T的两个节点p、q,最近公共祖先LCA(T,p,q)表示一个节点x,满足x是p和q的祖先且x的深度尽可能大。在这里,一个节点也可以是它自己的祖先.
2.二叉搜索树是若它的左子树不空,则左子树上所有节点的值均小于它的根节点的值; 若它的右子树不空,则右子树上所有节点的值均大于它的根节点的值
3.所有节点的值都是唯一的。
4.p、q 为不同节点且均存在于给定的二叉搜索树中。

(2)搜索路径比较(推荐)
二叉搜索树没有相同值的节点,因此分别从根节点往下利用二叉搜索树较大的数在右子树,较小的数在左子树,可以轻松找到p、q:

//节点值都不同,可以直接用值比较
while(node.val != target) { 
    path.add(node.val);
    //小的在左子树
    if(target < node.val) 
        node = node.left;
    //大的在右子树
    else 
        node = node.right;
}

直接得到从根节点到两个目标节点的路径,这样我们利用路径比较就可以找到最近公共祖先。

step 1:根据二叉搜索树的性质,从根节点开始查找目标节点,当前节点比目标小则进入右子树,当前节点比目标大则进入左子树,直到找到目标节点。这个过程成用数组记录遇到的元素。
step 2:分别在搜索二叉树中找到p和q两个点,并记录各自的路径为数组。
step 3:同时遍历两个数组,比较元素值,最后一个相等的元素就是最近的公共祖先。

import java.util.*;
public class Solution {
    //求得根节点到目标节点的路径
    public ArrayList<Integer> getPath(TreeNode root, int target) {
        ArrayList<Integer> path = new ArrayList<Integer>();
        TreeNode node = root;
        //节点值都不同,可以直接用值比较
        while(node.val != target){ 
            path.add(node.val);
            //小的在左子树
            if(target < node.val) 
                node = node.left;
            //大的在右子树
            else 
                node = node.right;
        }
        path.add(node.val);
        return path;
    }
    public int lowestCommonAncestor (TreeNode root, int p, int q) {
        //求根节点到两个节点的路径
        ArrayList<Integer> path_p = getPath(root, p); 
        ArrayList<Integer> path_q = getPath(root, q);
        int res = 0;
        //比较两个路径,找到第一个不同的点
        for(int i = 0; i < path_p.size() && i < path_q.size(); i++){ 
            int x = path_p.get(i);
            int y = path_q.get(i);
            //最后一个相同的节点就是最近公共祖先
            if(x == y) 
                res = path_p.get(i);
            else
                break;
        }
        return res;
    }
}

时间复杂度:O(n),设二叉树共有n个节点,因此最坏情况二叉搜索树变成链表,搜索到目标节点需要O(n),比较路径前半段的相同也需要O(n)
空间复杂度:O(n),记录路径的数组最长为n

(3)解法二:递归(一次遍历)
我们也可以利用二叉搜索树的性质:对于某一个节点若是p与q都小于等于这个这个节点值,说明p、q都在这个节点的左子树,而最近的公共祖先也一定在这个节点的左子树;若是p与q都大于等于这个节点,说明p、q都在这个节点的右子树,而最近的公共祖先也一定在这个节点的右子树。而若是对于某个节点,p与q的值一个大于等于节点值,一个小于等于节点值,说明它们分布在该节点的两边,而这个节点就是最近的公共祖先,因此从上到下的其他祖先都将这个两个节点放到同一子树,只有最近公共祖先会将它们放入不同的子树,每次进入一个子树又回到刚刚的问题,因此可以使用递归。

step 1:首先检查空节点,空树没有公共祖先。
step 2:对于某个节点,比较与p、q的大小,若p、q在该节点两边说明这就是最近公共祖先。
step 3:如果p、q都在该节点的左边,则递归进入左子树。
step 4:如果p、q都在该节点的右边,则递归进入右子树。

import java.util.*;
public class Solution {
    public int lowestCommonAncestor (TreeNode root, int p, int q) {
        //空树找不到公共祖先
        if(root == null) 
            return -1;
        //pq在该节点两边说明这就是最近公共祖先
        if((p >= root.val && q <= root.val) || (p <= root.val && q >= root.val)) 
            return root.val;
        //pq都在该节点的左边
        else if(p <= root.val && q <= root.val) 
            //进入左子树
            return lowestCommonAncestor(root.left, p, q); 
        //pq都在该节点的右边
        else 
            //进入右子树
            return lowestCommonAncestor(root.right, p, q); 
    }
}

(五)动态规划(递归、斐波那契)

(1)《剑指offer》7-斐波那契数列【递归、斐波那契】

(1)题目描述:
大家都知道斐波那契数列,现在要求输入一个整数n,请你输出斐波那契数列的第n项(从0开始,第0项为0)。n<=39

斐波那契数列:0,1,1,2,3,5,8… 总结起来就是:第一项是0,第二项是1,后续第n项为第n-1项和第n-2项之和。
用公式描述如下:
在这里插入图片描述

(2)解题思路:
看到这个公式,非常自然的可以想到直接用递归解决。但是这里存在一个效率问题,以求f(10)为例,需要先求出前两项f(9)和f(8),同样求f(9)的时候又需要求一次f(8),这样会导致很多重复计算,下图可以直观的看出。重复计算的结点数会随着n的增加而急剧增加,导致严重的效率问题。

因此,可以不使用递归,直接使用简单的循环方法实现。
在这里插入图片描述
(3)解法一:递归法
斐波那契数列的标准公式为:F(1)=1,F(2)=1, F(n)=F(n-1)+F(n-2)(n>=3,n∈N*)
根据公式可以直接写出:

public class Solution {
    public int Fibonacci(int n) {
        if(n<=1){
            return n;
        }
        return Fibonacci(n-1) + Fibonacci(n-2);
    }
}

在这里插入图片描述

(4)解法二:优化递归
递归会重复计算大量相同数据,我们用个数组把结果存起来8!

public class Solution {
    public int Fibonacci(int n) {
        int ans[] = new int[40];
        ans[0] = 0;
        ans[1] = 1;
        for(int i=2;i<=n;i++){
            ans[i] = ans[i-1] + ans[i-2];
        }
        return ans[n];
    }
}

时间复杂度:O(n)
空间复杂度:O(n)

(5)解法三:优化存储
其实我们可以发现每次就用到了最近的两个数,所以我们可以只存储最近的两个数
sum 存储第 n 项的值
one 存储第 n-1 项的值
two 存储第 n-2 项的值

public class Solution {
    public int Fibonacci(int n) {
        if(n == 0){
            return 0;
        }else if(n == 1){
            return 1;
        }
        int sum = 0;
        int two = 0;
        int one = 1;
        for(int i=2;i<=n;i++){
            sum = two + one;
            two = one;
            one = sum;
        }
        return sum;
    }
}

时间复杂度:O(n)
空间复杂度:O(1)

(6)解法四:持续优化
观察上一版发现,sum 只在每次计算第 n 项的时候用一下,其实还可以利用 sum 存储第 n-1 项,例如当计算完 f(5) 时 sum 存储的是 f(5) 的值,当需要计算 f(6) 时,f(6) = f(5) + f(4),sum 存储的 f(5),f(4) 存储在 one 中,由 f(5)-f(3) 得到
在这里插入图片描述

public class Solution {
    public int Fibonacci(int n) {
        if(n == 0){
            return 0;
        }else if(n == 1){
            return 1;
        }
        int sum = 1;
        int one = 0;
        for(int i=2;i<=n;i++){
            sum = sum + one;
            one = sum - one;
        }
        return sum;
    }
}

时间复杂度:O(n)
空间复杂度:O(1)

(2)《剑指offer》8-青蛙跳台阶【斐波那契,递归】

(1)题目描述:
一只青蛙一次可以跳上1级台阶,也可以跳上2级。求该青蛙跳上一个n级的台阶总共有多少种跳法(先后次序不同算不同的结果)。

(2)解题思路:
首先考虑最简单的情况,如果只有1级台阶,显然只有一种跳法。如果有两级台阶,就有两种跳法:一种是分两次跳,一种是一次跳两级。

在一般情况下,可以把n级台阶的跳法看成n的函数,记为f(n),那么一般情况下,一开始我们有两种不同的选择:(1)第一步只跳一级,此时跳法数目等于后面剩下的n-1级台阶的跳法数目,即f(n-1);(2)第一步跳两级,那么跳法数目等于后面剩下的n-2级台阶的跳法数目,即f(n-2)。所以f(n)=f(n-1)+f(n-2)。

至此,我们不难看出本题实际上就是求斐波那契数列,直接按照第7题思路便可以解决。
在这里插入图片描述

(3)解法一:递归法
题目分析,假设f[i]表示在第i个台阶上可能的方法数。逆向思维。如果我从第n个台阶进行下台阶,下一步有2中可能,一种走到第n-1个台阶,一种是走到第n-2个台阶。所以f[n] = f[n-1] + f[n-2]. 那么初始条件了,f[0] = f[1] = 1。 所以就变成了:f[n] = f[n-1] + f[n-2], 初始值f[0]=1, f[1]=1,目标求f[n] 看到公式很亲切,代码秒秒钟写完。

我们也可以根据公式倒推,因为F(n)=F(n−1)+F(n−2),而F(n−1)与F(n−2)又可以作为子问题继续计算,因此可以使用递归。

终止条件: 当递归到数列第1项或是第0项的时候,可以直接返回数字。
返回值: 返回这一级子问题的数列值。
本级任务: 获取本级数列值:即前两项相加。

step 1:低于2项的数列,直接返回1。
step 2:对于当前n,递归调用函数计算两个子问题相加。



public class Solution {
    public int JumpFloor(int target) {
        if(target<=2){
            return target;
        }
        return JumpFloor(target-1)+JumpFloor(target-2);
    }
}

时间复杂度:O(2^n) ,每个递归会调用两个递归,因此呈现2的指数增长
空间复杂度:O(n), 栈空间最大深度为n

优点,代码简单好写
缺点:慢,会超时 时间复杂度:O(2^n) 空间复杂度:递归栈的空间
在这里插入图片描述通过图会发现,方法一中,存在很多重复计算,因为为了改进,就把计算过的保存下来。 那么用什么保存呢?一般会想到map, 但是此处不用牛刀,此处用数组就好了。

step 1:使用dp数组记录前面的数列值。
step 2:函数中低于2项的数列,直接返回1。
step 3:对于当前n,如果dp数组中存在则直接使用,否则递归调用函数计算两个子问题相加。

public class Solution {
    //设置全局变量,因为实际问题中没有0,则可用0作初始标识值
    private int[] dp = new int[50];
    public int F(int n){
        if(n <= 1)
            return 1;
        //若是dp中有值则不需要重新递归加一次
        if(dp[n] != 0)   
            return dp[n];
        //若是dp中没有值则需要重新递归加一次
        return dp[n] = F(n - 1) + F(n - 2);
    }
    public int jumpFloor(int target) {
        return F(target);
    }
}

时间复杂度:每个数字只算了一次,故为O(n)
空间复杂度:O(n),栈空间最大深度

(3)解法二:迭代相加(推荐使用)
如果想让空间继续优化,那就用动态规划,优化掉递归栈空间。 方法二是从上往下递归的然后再从下往上回溯的,最后回溯的时候来合并子树从而求得答案。 那么动态规划不同的是,不用递归的过程,直接从子树求得答案。过程是从下往上。

将待求解的问题分解成若干个相互联系的子问题,先求解子问题,然后从这些子问题的解得到原问题的解;对于重复出现的子问题,只在第一次遇到的时候对它进行求解,并把答案保存起来,让以后再次遇到时直接引用答案,不必重新求解。动态规划算法将问题的解决方案视为一系列决策的结果。本解法属于动态规划空间优化方法

一只青蛙一次可以跳1阶或2阶,直到跳到第n阶,也可以看成这只青蛙从n阶往下跳,到0阶,按照原路返回的话,两种方法事实上可以的跳法是一样的——即怎么来的,怎么回去! 当青蛙在第n阶往下跳,它可以选择跳1阶到n−1,也可以选择跳2阶到n−2,即它后续的跳法变成了f(n−1)+f(n−2),这就变成了斐波那契数列。因此可以按照斐波那契数列的做法来做:即输入n,输出第n个斐波那契数列的值,初始化0阶有1种,1阶有1种。

step 1:低于2项的数列,直接返回n。
step 2:初始化第0项,与第1项分别为0,1.
step 3:从第2项开始,逐渐按照公式累加,并更新相加数始终为下一项的前两项。

public class Solution {
    public int jumpFloor(int target) {
        //从0开始,第0项是0,第一项是1
        if(target <= 1)   
            return 1;
        int res = 0;
        int a = 0;
        int b = 1;
        //因n=2时也为1,初始化的时候把a=0,b=1
        for(int i = 2; i <= target; i++){
        //第三项开始是前两项的和,然后保留最新的两项,更新数据相加
            res = (a + b);
            a = b;
            b = res;
        }
        return res;
    }
}

时间复杂度:O(n),其中nnn为输入的数
空间复杂度:O(1),常数级变量,没有其他额外辅助空间

(4)解法三:动态规划
既然与斐波那契数列相同,我们就把它放入数组中来解决。

step 1:创建一个长度为n+1的数组,因为只有n+1才能有下标第n项,我们用它记录前n项斐波那契数列。
step 2:根据公式,初始化第0项和第1项。
step 3:遍历数组,依照公式某一项等于前两项之和,将数组后续元素补齐,即可得到fib[n]。

public class Solution {
    public int jumpFloor(int target) {
        //从0开始,第0项是1,第一项是1
        if (target <= 1)    
             return 1;
        int[] temp = new int[target + 1];
        //初始化
        temp[0] = 1;
        temp[1] = 1;
        //遍历相加
        for (int i = 2; i <= target; i++) 
            temp[i] = temp[i - 1] + temp[i - 2];
        return temp[target];
    }
}

时间复杂度:O(n),遍历了一次长度为nnn的数组
空间复杂度:O(n),建立了一个数组辅助

(3)《剑指offer》9-变态跳台阶【斐波那契,贪心】

(1)题目描述:
一只青蛙一次可以跳上1级台阶,也可以跳上2级……它也可以跳上n级。求该青蛙跳上一个n级的台阶总共有多少种跳法。

(2)解法一:数学规律
当只有一级台阶时,f(1)=1;当有两级台阶时,f(2)=f(2-1)+f(2-2);一般情况下,当有n级台阶时,f(n)=f(n-1)+f(n-2)+···+f(n-n)=f(0)+f(1)+···+f(n-1),同理,f(n-1)=f(0)+f(1)+···+f(n-2).

因此,根据上述规律可以得到:f(n)=2*f(n-1)。这时一个递推公式,同样为了效率问题,用循环可以实现。

在这里插入图片描述

public class Solution {
    public int JumpFloorII(int target) {
        return (int) Math.pow(2,target-1);
        //return target <= 0 ? 0 : 1 << (target - 1);
    }
}

时间复杂度:O(n),次方运算还是需要n-1次(n为number)
空间复杂度:O(1),常数级变量,无额外辅助空间

思路分析:
跳上n-1级台阶,可以从n-2级跳1级上去,也可以从n-3级跳2级上去
那么f(n-1)=f(n-2)+f(n-3)+…+f(0)
同样,跳上n级台阶,可以从n-1级跳上1级上去,也可以从n-2级跳2级上去,那么f(n)=f(n-1)+f(n-2)+…+f(0)
综上可得
f(n)-f(n-1)=f(n-1),即f(n)=2*f(n-1)
所以f(n)是一个等比数列

(3)解法二:递归
根据上述思路,我们还可以从后往前,因为f(n)=2∗f(n−1),相当于找到子问题,其答案的两倍就是父问题的答案。

终止条件: 递归进入0或者1,可以直接得到方案数为1.
返回值: 将本级子问题得到的方案数的两倍返回给父问题。
本级任务: 进入台阶数减1的子问题。

step 1:若是number为1或者0,直接放回1种方案数。
step 2:其他情况返回子问题答案的2倍。

在这里插入图片描述

public class Solution {
    public int jumpFloorII(int target) {
        //1或0都是1种
        if(target <= 1) 
            return 1;
        //f(n) = 2*f(n-1)
        return 2 * jumpFloorII(target - 1); 
    }
}

时间复杂度:O(n),递归公式为T(n)=T(n−1)+1
空间复杂度:O(n),递归栈最大深度为n

(4)解法三:动态规划(推荐使用)
在这里插入图片描述step 1:使用动态规划数组,下标i表示第i级台阶的方案数。
step 2:初始化前面两个,即0级一种,1级一种。
step 3:遍历后续,后一个是前一个的两倍。

public class Solution {
    public int jumpFloorII(int target) {
        int[] dp = new int[target + 1];
        //初始化前面两个
        dp[0] = 1; 
        dp[1] = 1;
        //依次乘2
        for(int i = 2; i <= target; i++) 
            dp[i] = 2 * dp[i - 1];
        return dp[target];
    }
}

时间复杂度:O(n),其中nnn为台阶数,一次遍历
空间复杂度:O(n),辅助数组dp的长度为n

(4)《剑指offer》10-矩形覆盖【递归】

(1)题目描述:我们可以用21的小矩形横着或者竖着去覆盖更大的矩形。请问用n个21的小矩形无重叠地覆盖一个2n的大矩形,总共有多少种方法?
在这里插入图片描述比如n=3时,2
3的矩形块有3种不同的覆盖方法(从同一个方向看):

(2)解题思路:
我们可以以2 X 8的矩形为例。

先把2X8的覆盖方法记为f(8),用1X2的小矩形去覆盖时,有两种选择:横着放或者竖着放。当竖着放时,右边还剩下2X7的区域。很明显这种情况下覆盖方法为f(7)。当横着放时,1X2的矩形放在左上角,其下方区域只能也横着放一个矩形,此时右边区域值剩下2X6的区域,这种情况下覆盖方法为f(6)。所以可以得到:f(8)=f(7)+f(6),不难看出这仍然是斐波那契数列。

特殊情况:f(1)=1,f(2)=2

(3)解法一:斐波那契的递归

public class Solution {
    public int RectCover(int target) {

    if(target <=2){
            return target;
    }else{
            return RectCover(target-1) + RectCover(target-2);
       }
    }
}

(4)解法二:

public class Solution {
    public int RectCover(int target) {
        if (target <= 2){
            return target;
        }
        int pre1 = 2; // n 最后使用一块,剩下 n-1 块的写法
        int pre2 = 1; // n 最后使用两块,剩下 n-2 块的写法
        for (int i = 3; i <= target; i++){
            int cur = pre1 + pre2;
            pre2 = pre1;
            pre1 = cur;
        }
        return pre1; //相对于 n+1 块来说,第 n 种的方法
    }
}

(5)《剑指offer》

(六)栈和队列

(1)《剑指offer》5-两个栈实现一个队列【栈+队列】

(1)题目: 用两个栈来实现一个队列,完成队列的Push和Pop操作。 队列中的元素为int类型。

(2)解法:
元素进栈以后,只能优先弹出末尾元素,但是队列每次弹出的却是最先进去的元素,如果能够将栈中元素全部取出来,才能访问到最前面的元素,此时,可以用另一个栈来辅助取出。

step 1:push操作就正常push到第一个栈末尾。
step 2:pop操作时,优先将第一个栈的元素弹出,并依次进入第二个栈中。

//将第一个栈中内容弹出放入第二个栈中
while(!stack1.isEmpty())
stack2.push(stack1.pop());

step 3:第一个栈中最后取出的元素也就是最后进入第二个栈的元素就是队列首部元素,要弹出,此时在第二个栈中可以直接弹出。
step 4:再将第二个中保存的内容,依次弹出,依次进入第一个栈中,这样第一个栈中虽然取出了最里面的元素,但是顺序并没有变。

//再将第二个栈的元素放回第一个栈
while(!stack2.isEmpty())
stack1.push(stack2.pop());

在这里插入图片描述

import java.util.Stack;
public class Solution {
    Stack<Integer> stack1 = new Stack<Integer>();
    Stack<Integer> stack2 = new Stack<Integer>();
    
    public void push(int node) {
        stack1.push(node);
    }
    
    public int pop() {
        //将第一个栈中内容弹出放入第二个栈中
        while(!stack1.isEmpty()) 
            stack2.push(stack1.pop()); 
        //第二个栈栈顶就是最先进来的元素,即队首
        int res = stack2.pop(); 
        //再将第二个栈的元素放回第一个栈
        while(!stack2.isEmpty()) 
            stack1.push(stack2.pop());
        return res;
    }
}

思路分析:
栈1是先入后出,栈2是先入后出,队列是先入先出
当队列入的时候,就直接入栈1,当队列出的时候,分两种情况
情况一;当栈2不为空的时候,就直接从栈2里出
情况二:当栈2位空的时候,那就先把栈1里的出到栈2,再从栈2里出

时间复杂度:push的时间复杂度为O(1),pop的时间复杂度为O(n),push是直接加到栈尾,相当于遍历了两次栈
空间复杂度:O(n),借助了另一个辅助栈空间

(2)《剑指offer》20-包含min函数的栈【双栈】

(1)题目: 定义栈的数据结构,请在该类型中实现一个能够得到栈中所含最小元素的min函数(时间复杂度应为O(1))。

在这里插入图片描述

(2)注意:保证测试中不会当栈为空的时候,对栈调用pop()或者min()或者top()方法。

import java.util.Stack;

public class Solution {
    
    private static Stack<Integer> stack = new Stack<Integer>();
    private static Integer minElement = Integer.MAX_VALUE;
    
    public void push(int node) {
        if(stack.empty()){
            minElement = node;
            stack.push(node);
        }else{
            if(node <= minElement){
                stack.push(minElement);//在push更小的值时需要保留在此值之前的最小值
                minElement = node;
            }
            stack.push(node);
        }
    }
    
    public void pop() {
        if(stack.size() == 0)return;
        if( minElement == stack.peek()){
            if(stack.size() >1){
                stack.pop();
                minElement = stack.peek();
            }else{
                minElement = Integer.MAX_VALUE;
            }
 
        }
        stack.pop();
    }
    
    public int top() {
        return stack.peek();
    }
    
    public int min() {
        return minElement;
    }
}

(3)《剑指offer》21-栈的压入、弹出序列

(1)题目:
输入两个整数序列,第一个序列表示栈的压入顺序,请判断第二个序列是否可能为该栈的弹出顺序。假设压入栈的所有数字均不相等。例如序列1,2,3,4,5是某栈的压入顺序,序列4,5,3,2,1是该压栈序列对应的一个弹出序列,但4,3,5,1,2就不可能是该压栈序列的弹出序列。(注意:这两个序列的长度是相等的)
在这里插入图片描述
(2)解法一:辅助栈
题目要我们判断两个序列是否符合入栈出栈的次序,我们就可以用一个栈来模拟。对于入栈序列,只要栈为空,序列肯定要依次入栈。那什么时候出来呢?自然是遇到一个元素等于当前的出栈序列的元素,那我们就放弃入栈,让它先出来。

//入栈:栈为空或者栈顶不等于出栈数组
while(j < n && (s.isEmpty() || s.peek() != popA[i])){
s.push(pushA[j]);
j++;
}

如果能按照这个次序将两个序列都访问完,那说明是可以匹配入栈出栈次序的。

step 1:准备一个辅助栈,两个下标分别访问两个序列。
step 2:辅助栈为空或者栈顶不等于出栈数组当前元素,就持续将入栈数组加入栈中。
step 3:栈顶等于出栈数组当前元素就出栈。
step 4:当入栈数组访问完,出栈数组无法依次弹出,就是不匹配的,否则两个序列都访问完就是匹配的。

import java.util.Stack;
public class Solution {
    public boolean IsPopOrder(int [] pushA,int [] popA) {
        int n = pushA.length;
        //辅助栈
        Stack<Integer> s = new Stack<>();
        //遍历入栈的下标
        int j = 0;
        //遍历出栈的数组
        for(int i = 0; i < n; i++){
            //入栈:栈为空或者栈顶不等于出栈数组
            while(j < n && (s.isEmpty() || s.peek() != popA[i])){
                s.push(pushA[j]);
                j++;
            }
            //栈顶等于出栈数组
            if(s.peek() == popA[i])
                s.pop();
            //不匹配序列
            else
                return false;
        }
        return true;
    }
}

时间复杂度:O(n),其中n为数组长度,最坏情况下需要遍历两个数组各一次
空间复杂度:O(n),辅助栈空间最大为一个数组的长度

另一种写法

import java.util.ArrayList;
import java.util.Stack;
public class Solution {
    public boolean IsPopOrder(int [] pushA,int [] popA) {

        if (pushA.length == 0 || popA.length == 0 || popA.length != pushA.length)
            return false;
        Stack<Integer> stack = new Stack<Integer>();
        int j = 0;
        for (int i = 0; i < pushA.length; i++) {
            stack.push(pushA[i]);
            while (!stack.isEmpty() && stack.peek() == popA[j]){
                stack.pop();
                j++;
            }
        }
        return stack.isEmpty();
    }
}

(3)解法二:原地栈(扩展思路)
方法一我们使用了一个辅助栈来模拟,但是数组本来就很类似栈啊,用下标表示栈顶。在方法一种push数组前半部分入栈了,就没用了,这部分空间我们就可以用来当成栈。原理还是同方法一一样,只是这时我们遍历push数组的时候,用下标n表示栈空间,n的位置就是栈顶元素。

step 1:用下标n表示栈空间,用j表示出栈序列的下标。
step 2:遍历每一个待入栈的元素,加入栈顶,即push数组中n的位置,同时增加栈空间的大小,即n的大小。
step 3:当栈不为空即栈顶n大于等于0,且栈顶等于当前出栈序列,就出栈,同时缩小栈的空间,即减小n。
step 4:最后若是栈空间大小n为0,代表全部出栈完成,否则不匹配。

public class Solution {
    public boolean IsPopOrder(int [] pushA,int [] popA) {
        //表示栈空间的大小,初始化为0
        int n = 0;
        //出栈序列的下标
        int j = 0;
        //对于每个待入栈的元素
        for(int num : pushA){
            //加入栈顶
            pushA[n] = num;
            //当栈不为空且栈顶等于当前出栈序列
            while(n >= 0 && pushA[n] == popA[j]){
                //出栈,缩小栈空间
                j++;
                n--;
            }
            n++;
        }
        //最后的栈是否为空
        return n == 0;
    }
}

时间复杂度:O(n),其中nnn为数组长度,最坏还是遍历两个数组
空间复杂度:O(1),常数级变量,无额外辅助空间

(5)滑动窗口的最大值

(七)回溯

(1)《剑指offer》矩阵中的路径

(1)题目描述:
在这里插入图片描述
(2)解法
回溯算法实际上一个类似枚举的搜索尝试过程,也就是一个个去试,我们解这道题也是通过一个个去试,下面就用示例1来画个图看一下
在这里插入图片描述
我们看到他是从矩形中的一个点开始往他的上下左右四个方向查找,这个点可以是矩形中的任何一个点,所以代码的大致轮廓我们应该能写出来,就是遍历矩形所有的点,然后从这个点开始往他的4个方向走,因为是二维数组,所以有两个for循环,代码如下

public boolean hasPath (char[][] matrix, String word) {
    char[] words = word.toCharArray();
    for (int i = 0; i < matrix.length; i++) {
        for (int j = 0; j < matrix[0].length; j++) {
            //从[i,j]这个坐标开始查找
            if (dfs(matrix, words, i, j, 0))
                return true;
        }
    }
    return false;
}

这里关键代码是dfs这个函数,因为每一个点我们都可以往他的4个方向查找,所以我们可以把它想象为一棵4叉树,就是每个节点有4个子节点,而树的遍历我们最容易想到的就是递归,我们来大概看一下

boolean dfs(char[][] board, char[] word, int i, int j, int index) {
    if (边界条件的判断) {
        return;
    }

    一些逻辑处理

    boolean res;
    //往右
    res = dfs(board, word, i + 1, j, index + 1)
    //往左
    res |= dfs(board, word, i - 1, j, index + 1)
    //往下
    res |= dfs(board, word, i, j + 1, index + 1)
    //往上
    res |= dfs(board, word, i, j - 1, index + 1)
    //上面4个方向,只要有一个能查找到,就返回true;
    return res;
}

最终的完整代码如下

    public boolean hasPath(char[][] matrix, String word) {
        char[] words = word.toCharArray();
        for (int i = 0; i < matrix.length; i++) {
            for (int j = 0; j < matrix[0].length; j++) {
                //从[i,j]这个坐标开始查找
                if (dfs(matrix, words, i, j, 0))
                    return true;
            }
        }
        return false;
    }

    boolean dfs(char[][] matrix, char[] word, int i, int j, int index) {
        //边界的判断,如果越界直接返回false。index表示的是查找到字符串word的第几个字符,
        //如果这个字符不等于matrix[i][j],说明验证这个坐标路径是走不通的,直接返回false
        if (i >= matrix.length || i < 0 || j >= matrix[0].length || j < 0 || matrix[i][j] != word[index])
            return false;
        //如果word的每个字符都查找完了,直接返回true
        if (index == word.length - 1)
            return true;
        //把当前坐标的值保存下来,为了在最后复原
        char tmp = matrix[i][j];
        //然后修改当前坐标的值
        matrix[i][j] = '.';
        //走递归,沿着当前坐标的上下左右4个方向查找
        boolean res = dfs(matrix, word, i + 1, j, index + 1)
                || dfs(matrix, word, i - 1, j, index + 1)
                || dfs(matrix, word, i, j + 1, index + 1)
                || dfs(matrix, word, i, j - 1, index + 1);
        //递归之后再把当前的坐标复原
        matrix[i][j] = tmp;
        return res;
    }

时间复杂度:O(mn*k^3),m和n是矩阵的宽和高,最坏的情况下遍历矩阵的所有位置,k是字符串的长度,下面的dfs我们可以把它看做是一棵4叉树,除了第一次的时候可以往4个方向走,其他情况下只能往3个方向走(进来的那个方向回不去)
空间复杂度:O(K),k是字符串的长度就k

(2)《剑指offer》机器人的运动范围

(八)数学

(1)《剑指offer》12-数值的整数次方

题目:给定一个double类型的浮点数base和int类型的整数exponent。求base的exponent次方。保证base和exponent不同时为0

解题思路:

本题看似比较简单,是一个简单的指数运算,但需要完整的考虑到所有情况。首先,对于底数,如果底数为0,则0的任何次方都是0,可以直接返回0。关键在于指数,指数可能有三种情况,有可能是正数、0、负数。对于指数是0的情况,任何数的0次方为1。对于指数是负数的情况,可以将其转化为绝对值计算,求出结果之后再求倒数。

在计算n次方的时候,为了方便,我们根据减治的思想,通过同底数指数幂的公式计算,如下列公式。这里需要注意的是奇数和偶数的不同。
在这里插入图片描述这里为了简单,我们通过递归直接实现,因此也可以不求负数的绝对值,因为一个数每次除以2,最终只有可能是0,1或者-1,将其作为递归结束条件,可以直接写出以下简洁代码。

解法一:

public class Solution {
    public double Power(double base, int exponent) {
        return Math.pow(base,exponent);
    }
}

Math工具类中pow方法就是用来计算次方的

解法二:

class Solution {
    public double myPow(double x, int n) {
        if(n==0) return 1;
        if(n==1) return x;
        if(n==-1) return 1/x;
        double qi = myPow(x,n/2);
        double ou = myPow(x,n%2);
        return qi * qi * ou;       //这里qi,ou写错了,不是奇数和偶数的意思,他的意思是不管你奇数还是偶数,都需要先求n/2,还得求两次,最后再求多余的指数即n%2,所以等于 qi * qi * ou(看上面为奇数的公式,当n为偶数,n%2为0,ou为1,不影响计算结果)
    }
}

(2)《剑指offer》47 求1+2+3+……+n

题目描述:

求1+2+3+…+n,要求不能使用乘除法、for、while、if、else、switch、case等关键字及条件判断语句(A?B:C)。

解题思路:

本题本身没有太多的实际意义,但是可以对程序员的发散思维能力进行考察,进而可以反映出对编程相关技术理解的深度。

对于本题,书中给出了利用构造函数、虚函数、函数指针、模板类型求解等思路,在这里,若使用java实现,有些方法却是不适用的,比如构造函数法,java构造对象数组并不会多次调用构造函数,其他方法略显复杂,这里我们给出另外一个思路:

可以通过递归来实现加法,但是由于无法使用if语句,因此对于递归的结束条件无法进行判断,这里用一个比较巧妙的思路:与运算的短路特性,所谓短路,比如 A && B,当A条件不成立时,不论B是否成立,结果都是false,所以B不再进行计算,利用短路特性可以实现递归停止,进而求出和。
在这里插入图片描述

public int sumNUMs(int n) {
    /*
    等差数列求和:S=n(n+1)/2  无法使用
    1.需利用逻辑与的短路特性实现递归终止。 
    2.当n==0时,(n>0)&&((sum+=sumNUMs(n-1))>0)只执行前面的判断,为false,然后直接返回0;
 3.当n>0时,执行sum+=sumNUMs(n-1),实现递归计算sumNUMs(n)。
    */
    boolean t = (n > 1) && (n = n + sumNums(n - 1))!= 0;
    return n;   //最后的返回值是n,不是1,是因为每一层返回去的是N,而不是1,只有最后一层返回去的才是1
}

(3)《剑指offer》48 不用加减乘除做加法

题目描述:

写一个函数,求两个整数之和,要求在函数体内不得使用+、-、*、/四则运算符号。

解题思路:

本题同样是对发散思维能力的一个考察。首先,我们需要考虑是要求和却不能使用四则运算,那么还能用什么呢?除了四则运算以外,还可以进行计算的也就只剩下了位运算。因此,需要进一步考虑二进制数的位运算,用位运算来代替加法。

具体思路是:三步走策略。第一步,不考虑进位对每一位相加(模2和),也就是0+0=0,1+1=0,0+1=1,1+0=0,不难看出这一步其实就是做异或运算。第二步,考虑进位,只有1+1会产生进位,因此求每一位的进位可以先将两个数做与运算,然后再左移一位。第三步,将前面两个结果相加,相当于递归相加,直到不产生进位为止。

编程实现(Java):
(做的时候记得搜一下,二进制加法是怎么计算的,好理解好多)
二进制加法

public class Solution {
    /*
        思路:用位运算代替加法
        三步走:第一步:不考虑进位加 第二位:考虑进位 第三步:原结果加上进位
    */
    public int Add(int num1,int num2) {
        //递归实现
        if(num2==0) return num1;
        int sum=num1^num2;			//第一步,两个加数做异或运算,只算加法部分不考虑进位
        int carry=(num1&num2)<<1;	//第二步,两个加数做与运算,考虑进位
        return Add(sum,carry);		//第三步,把上面两个结果加起来,直到不用进位为止
    }
}

(4)《剑指offer》11-二进制中1的个数【进制转化,补码反码源码】

题目描述:编写一个函数,输入是一个无符号整数(以二进制串的形式),返回其二进制表达式中数字位数为 ‘1’ 的个数(也被称为 汉明重量).)。其中负数用补码表示。

解法一:

public int hammingWeight(int n) {
    int index = 32;
    int result = 0;
    for(int i=0;i<index;i++) {
        if(((n>>i) & 1) == 1) {
            result ++;
        }
    }
    return result;
}

解法二:

public class Solution {
    public int NumberOf1(int n) {
    	int count=0;
    	while(n!=0){
        count++;
        n=n&(n-1);
    	}
    	return count;
    }
}

(九)排序

《剑指offer》29-最小的k个数

题目:输入n个整数,找出其中最小的K个数。例如输入4,5,1,6,2,7,3,8这8个数字,则最小的4个数字是1,2,3,4,。

import java.util.ArrayList;
public class Solution {
    public ArrayList<Integer> GetLeastNumbers_Solution(int [] input, int k) {

        ArrayList<Integer> result = new ArrayList<Integer>();
        if(k<= 0 || k > input.length)return result;
        //初次排序,完成k个元素的排序
        for(int i = 1; i< k; i++){
            int j = i-1;
            int unFindElement = input[i];
            while(j >= 0 && input[j] > unFindElement){
                input[j+1] = input[j];
                j--;
            }
 
            input[j+1] = unFindElement;
        }
        //遍历后面的元素 进行k个元素的更新和替换
        for(int i = k; i < input.length; i++){
            if(input[i] < input[k-1]){
                int newK = input[i];
                int j = k-1;
                while(j >= 0 && input[j] > newK){
                    input[j+1] = input[j];
                    j--;
                }
                input[j+1] = newK;
            }
        }
        //把前k个元素返回
        for(int i=0; i < k; i++)
            result.add(input[i]);
        return result;
    }
}

(十)其他

参考文章:
剑指offer的66道题根据知识点的分类和解答:题目链接

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值