剑指offer4-解决面试题的思路

4.2 画图

题目-二叉树的镜像

操作给定的二叉树,将其变换为源二叉树的镜像。 

思路

 

求二叉树镜像的过程:1)交换根结点的左右子树;2)交换值为10的结点的左右子结点;3)交换值为6的结点的左右子结点。

总结这个过程就是:前序遍历这棵树的每个结点,如果遍历到的结点有子结点,就交换它的两个子结点。直到交换完所有非叶子结点的左右子结点。

解答

/**
public class TreeNode {
    int val = 0;
    TreeNode left = null;
    TreeNode right = null;
 
    public TreeNode(int val) {
        this.val = val;
 
    }
 
}
*/
public class Solution {
    public void Mirror(TreeNode root) {
        if((root==null) || (root.left==null && root.right==null))
            return;
        //交换根结点的左右子树
        TreeNode temp=root.left;
        root.left=root.right;
        root.right=temp;
        //交换原来根结点的左子结点的左右子结点
        if(root.left!=null)
            Mirror(root.left);
        //交换原来根结点的右子结点的左右子结点
        if(root.right!=null)
            Mirror(root.right);
    }
}

 

题目20-顺序打印矩阵

输入一个矩阵,按照从外向里以顺时针的顺序依次打印出每一个数字,例如,如果输入如下4 X 4矩阵: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 则依次打印出数字1,2,3,4,8,12,16,15,14,13,9,5,6,7,11,10.

  

以从外圈到内圈的顺序依次打印,把矩阵想象成若干个圈。设置一个循环,每一次打印矩阵中的一个圈。

需要考虑到如果矩阵不是N*N的,那么打印的最后一圈可能退化成只有一行、只有一列或者只有一个数字,因此打印这样的一圈就不需要4步。

分析打印时每一步的前提条件:

1)第一步无条件

2)第二步无条件

3)第三步时圈内至少有两行两列,即要求终止行号>起始行号外 && 还要求终止列号>起始列号。(也就是打印时当终止行号!=起始行号)

4)第四步时圈内至少有三行两列,即要求终止行号-起始行号>2 && 终止列号>起始列号。(也就是终止列号!=起始列号)

解答

import java.util.ArrayList;
public class Solution {
    public ArrayList<Integer> printMatrix(int [][] matrix) {
       if(matrix == null)
           return null;
        ArrayList<Integer> ret = new ArrayList<>();
        int r1=0, r2=matrix.length-1, c1=0, c2=matrix[0].length-1;
        while(r1<=r2 && c1<=c2){
       //从左往右打印一行
for(int i=c1; i<=c2; i++){ ret.add(matrix[r1][i]); }
  //从上往下打印一列
for(int i=r1+1;i<=r2;i++){ ret.add(matrix[i][c2]); }
  //从右往左打印一行
if(r1!=r2){ for(int i=c2-1;i>=c1;i--){ ret.add(matrix[r2][i]); } }
//从下往上打印一列
if(c1!=c2){ for(int i=r2-1;i>r1;i--){ ret.add(matrix[i][c1]); } } r1++; r2--; c2--; c1++; } return ret; } }

 

4.3 举例

题目-包含min函数的栈

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

思路

1.首先的想法是每次压入一个新元素进栈,将栈里的所有元素排序,让最小的元素位于栈顶,这样可以在O(1)时间得到最小元素。但这样不能保证最后压入栈的元素能先出栈,因此这是数据结构就是不是栈了。

2.第二种思路是在栈里添加一个存放最小元素的成员变量,每次压入一个新元素入栈,更新最小元素变量。但这样如果当前最小元素被弹出栈,就不能得到下一个最小元素,因此始终是存储了一个最小元素。

3.也就是我们不仅需要保存最小元素,还希望能够保存次小元素。那我们考虑构建一个辅助栈(把每次的最小元素保存在辅助栈),过程如下:

 

即每次都把最小元素压入辅助栈,从而保证辅助栈的栈顶一直都是最小元素。当最小元素从数据栈内被弹出后,辅助栈的栈顶元素也被弹出,此时辅助栈的新栈顶元素就是下一个最小值。

栈的基本操作:

进栈:stack.push(Object);//返回入栈的内容

      stack.add(Object);//返回true/false

出栈:stack.pop();//输出并删除栈顶元素

   stack.peek();//输出且不删除栈顶元素 

解答

import java.util.Stack;
 
public class Solution {
 
    //数据栈
    private Stack<Integer> dataStack = new Stack<>();
    //辅助栈
    private Stack<Integer> minStack = new Stack<>();
     
    public void push(int node) {
        dataStack.push(node);
//将最小元素压入辅助栈
if(minStack.size()==0 || node<minStack.peek()) minStack.push(node); else minStack.push(minStack.peek()); } public void pop() { minStack.pop(); dataStack.pop(); } public int top() { return dataStack.peek(); } public int min() { return minStack.peek(); } }

 

题目-栈的压入、弹出序列

输入两个整数序列,第一个序列表示栈的压入顺序,请判断第二个序列是否可能为该栈的弹出顺序。假设压入栈的所有数字均不相等。例如序列1,2,3,4,5是某栈的压入顺序,序列4,5,3,2,1是该压栈序列对应的一个弹出序列,但4,3,5,1,2就不可能是该压栈序列的弹出序列。(注意:这两个序列的长度是相等的) 

思路

建立一个辅助栈,把输入的第一个序列中的数字依次压入该辅助栈,并按照第二个序列的顺序依次从该栈中弹出数字(当该数字为栈顶数字时,可以顺利弹出)。

以弹出序列为4、5、3、2、1为例进行分析。压入顺序为1、2、3、4、5

首先第一个需要被弹出的数字是4,因此4需要先被压入到辅助栈中。那么按照压入顺序,要把4压入栈,1、2、3都需要被压入栈。此时栈中为1、2、3、4;

接着把4弹出栈,栈中剩下1、2、3;

接着需要被弹出的数字是5,那么5需要被压入栈,即将压入序列中4以后的数字压入辅助栈,直到5入栈。此时栈中为1、2、3、5;

接着把5弹出栈,栈中剩下1、2、3;

之后每弹出的数字都在栈顶,可以被顺利弹出。

 接下来分析弹出序列为4、3、5、1、2,压入序列为1、2、3、4、5.

出现当需要弹出一个数字时,该数字并不在栈顶,并且不在压入序列中的待入栈部分。说明不符合

通过举例分析,总结出判断一个序列是否为栈的弹出序列的规律:

1.如果下一个弹出数字为栈顶数字,则直接弹出;

2.如果下一个弹出数字不为栈顶数字,则将压栈序列中还未入栈的数字入栈,直到下一个弹出数字为栈顶数字;

3.如果所有数字都入栈仍未在栈顶找到下一个弹出数字,则该序列不可能为弹出序列;

4.如果所有数字都被顺利弹出,则为弹出序列。 

解答

import java.util.ArrayList;
import java.util.Stack;
 
public class Solution {
    public boolean IsPopOrder(int [] pushA,int [] popA) {
        int n=pushA.length;
        Stack<Integer> stack = new Stack<>();
        for(int pushIndex=0,popIndex=0;pushIndex<n;pushIndex++){
            //将压栈序列中还没入栈的数字压入辅助栈,直到把下一个需要弹出的数字压入栈顶为止
            stack.push(pushA[pushIndex]);
            //每压入一次数字,都判断一次当前数字是否为需要弹出的数字。
            //如果下一个弹出的数字在栈顶,直接弹出
            while(popIndex<n && !stack.isEmpty() && stack.peek()==popA[popIndex]){
                stack.pop();
                popIndex++;
            }
        }
        //根据处理完的序列是否为空,判断序列是否为弹出序列
        return stack.isEmpty();
    }
}

 

题目-从上往下打印二叉树

从上往下打印出二叉树的每个节点,同层节点从左至右打印。 

 打印顺序为:8、6、10、5、7、9、11

思路

按层打印的顺序,那么就应该先打印根结点,然后再打印根结点的子结点,再分别打印根结点的左右子节点的子结点。

那么我们打印一个结点时,就把该结点的左右子结点加入到数据容器中。

打印顺序就是先入先出,那么构成的数据容器为一个队列。

总结出打印的规律:

1.每次打印一个结点,如果该结点有子结点,就将该结点的子结点放到队列的末尾;

2.到队列的头部取出最早进入队列的结点,重复以上的打印操作;

3.直到队列中所有的结点都被打印出来为止。

队列的基本操作:

remove();//移除并返回队列头部的元素

poll();//移除并返回队列头部的元素,在用空集合调用时不会抛出异常,而是返回null。更适合容易出现异常条件的情况

offer();//添加一个元素并返回true。一些队列有大小限制,因此如果想在一个满的队列中加入一个新项,多出的项就会被拒绝。返回添加结果

add();//增加一个元素

peek();//返回队列头部的元素&&更适合容易出现异常条件的情况。队列为空时,返回null

element();//返回队列头部的元素&&队列为空时,抛出异常

解答

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> list = new ArrayList<>();
        Queue<TreeNode> queue = new LinkedList<>();
        queue.add(root);
        //直到队列中所有的结点都被打印
        while(!queue.isEmpty()){
            int size=queue.size();
            while(size-->0){
                //返回第一个元素,并删除
                TreeNode t = queue.poll();
                if(t==null)
                    continue;
                list.add(t.val);
                queue.add(t.left);
                queue.add(t.right);
            }
        }
        return list;
    }
}

 

题目-二叉搜索树的后序遍历序列

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

思路

 

这个是后序遍历序列5、7、6、9、11、10、8对应的二叉搜索树。(左右根)

可以看出:后序遍历的序列中,最后一个数字是树的根结点的值。数组中前面的数字可以分为两部分:第一部分是左子树结点的值,它们比根结点都小;第二部分是右子树结点的值,比根结点都大。

然后可以使用同样的判断方式确定数组左右子树对应的子树方式,是一个递归的过程。

所以发现规律就是:

  1)找到根结点;

  2)根据根结点,找到左子树结点和右子树的分割点;

  3)再判断现在得到的右子树是否都大于根结点,如果存在小于根结点的结点,则不符合规则。

解答

public class Solution {
    public boolean VerifySquenceOfBST(int [] sequence) {
        //后序遍历是左右根
        if(sequence == null || sequence.length == 0)
            return false;
        return verify(sequence, 0, sequence.length-1);
    }
     
    private boolean verify(int [] sequence, int first, int last){
        //
        if(last-first <= 1)
            return true;
        //根结点
        int rootVal = sequence[last];
        int curIndex = first;
        //找到左子树的最右结点,根结点大于左子树的结点
        while(curIndex < last && sequence[curIndex] <= rootVal)
            curIndex++;
        //如果右子树的结点 存在小于根节点的,则不符合
        for(int i=curIndex;i<last;i++){
            if(sequence[i]<rootVal)
                return false;
        }
        //查找左子树和右子树是否符合后序遍历
        return verify(sequence, first, curIndex-1) && verify(sequence, curIndex, last-1);
    }
     
}

 

题目-二叉树中和为某一值的路径

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

如果输入该二叉树与22,那么输出两条路径:10、5、7;10、12.

路径是从根结点出发到叶结点,也就是路径总是以根结点为起始点。

因此我们可以得到规律:

1)当用前序遍历的方式访问某一结点时,就把该结点添加到路径上,并累加该结点的值;

2)如果累加的和 正好为 输入的整数,且刚添加的结点为叶结点时,则当前的路径符合要求,打印出来;

3)如果刚添加的不是叶结点,那么就继续访问它的子结点;(符合要求就打印)

4)当前结点结束访问时,就自动回到它的父结点。并在路径上删除这个结点,并且减去当前结点的值。使得现在的路径为根结点到父结点,因为这条路径走完,可以走父结点的另外子结点的路径了。

5)依次递归。

解答

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>> ret = new ArrayList<>();
    public ArrayList<ArrayList<Integer>> FindPath(TreeNode root,int target) {
        backtracking(root, target, new ArrayList<>());
        return ret;
    }
     
    private void backtracking(TreeNode node, int target, ArrayList<Integer> path){
        if(node==null)
            return;
        path.add(node.val);
        target=target-node.val;
        //在当前根结点就已经找到了一条路径。符合1.结点为整数之和;2.叶子结点
        if(target==0 && node.left==null && node.right==null)
            ret.add(new ArrayList<>(path));
        //如果不是叶子结点,就遍历子节点
        else{
            //沿着左子树继续查找,还未找到路径
            backtracking(node.left, target, path);
            //沿着右子树查找
            backtracking(node.right, target, path);
        }
        //当遍历到了叶子结点,即当前结点访问结束后,递归函数将自动回到它的父结点
        //在返回到父结点之前,在路径上删除当前结点,由于这一条路径不符合条件,返回当前结点的上一级。以确保返回父结点时路径刚好是从根结点到父结点的路径
        //即删除下标为path.size()-1的元素,就是刚添加的;如果是remove(Integer.valueOf(n)),就是删除值为n的元素
        path.remove(path.size()-1);
    }
}

 

4.4 分解

题目-复杂链表的复制

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

 实线箭头表示next指针,虚线箭头表示random指针 

思路

1)根据原始链表的每个结点N创建对应的N‘。把N’链接在N的后面; 

2)设置复制出来的结点N‘的指向。若原来的结点N的random是指向结点S,那么复制出来的结点N’的random是指向S‘(S’为S复制出来的下一结点)


3)把这个长链表进行拆分:奇数位置上的结点组成原始链表,偶数位置上的结点组成复制出来的链表。

 

另一种时间空间复杂度更高一些的做法是:创建一个哈希表,用来存储<N,N'>的配对信息。如果原来链表的N指向S,那么N'就是指向S'。可以通过哈希表在O(1)时间根据S找到S',但这样需要一个大小为O(n)的哈希表,用空间换时间。

解答

/*
public class RandomListNode {
    int label;
    RandomListNode next = null;
    RandomListNode random = null;
 
    RandomListNode(int label) {
        this.label = label;
    }
}
*/
public class Solution {
    public RandomListNode Clone(RandomListNode pHead)
    {
        if(pHead == null)
            return null;
        //在每个节点后面插入复制的节点
        RandomListNode cur = pHead;
        while(cur != null){
            //创建新节点
            RandomListNode clone = new RandomListNode(cur.label);
            //把新节点链接到原节点的后面
            clone.next = cur.next;
            cur.next = clone;
            cur = clone.next;
        }
        //对复制节点的random链接进行赋值
        cur = pHead;
        while(cur != null){
            //原始链表上的节点N的random指向S,那么其对应复制出来的N'是N的next指向的节点
            RandomListNode clone = cur.next;
            if(cur.random != null)
                //S'也是S的next指向的节点
                clone.random=cur.random.next;
            cur = clone.next;
        }
        //拆分
        cur = pHead;
        RandomListNode pCloneHead = pHead.next;
        while(cur.next != null){
            //隔一位取
            RandomListNode next = cur.next;
            cur.next = next.next;
            cur = next;
        }
        return pCloneHead;
    }
}

 

题目-二叉搜索树与双向链表

输入一棵二叉搜索树,将该二叉搜索树转换成一个排序的双向链表。要求不能创建任何新的结点,只能调整树中结点指针的指向。 

思路

二叉树中,每个结点都有两个指向子结点的指针。在双向链表中,每个结点也有两个指针,分别指向前一个结点和后一个结点。可以发现二叉搜索树与双向链表的相似性。转换时可以将原来指向右子结点的指针调整为链表中指向后一个结点指针。

并且转换后的链表是有序的,那么我们可以使用中序遍历来实现,中序遍历是左根右,即按从小到大的顺序遍历二叉树的每个结点。

按照中序遍历的顺序,我们遍历转换到根结点时,它的左子树已经转换为一个排序的链表了,并且处在链表中的最后一个结点时当前值最大的结点。

我们把值为8的结点和根结点链接起来,此时链表中的最后一个结点就是10.

接着再转换右子树,将根结点与右子树中最小的结点链接起来。

 那么左右子树的转换,依然使用递归完成。

解答

/**
public class TreeNode {
    int val = 0;
    TreeNode left = null;
    TreeNode right = null;
 
    public TreeNode(int val) {
        this.val = val;
 
    }
 
}
*/
public class Solution {
  //定义链表当前结点
private TreeNode pre=null;
  //定义链表头部的结点
private TreeNode head=null; public TreeNode Convert1(TreeNode pRootOfTree) { inOrder(pRootOfTree); return head; } private void inOrder(TreeNode node){ if(node==null) return; inOrder(node.left); //经过上一步,此时左子树已经转换为一个排序的链表了,而且链表的最后一个结点是当前值最大的结点 //接下来把左子树最大结点(当前链表最后一个结点)与根结点链接 node.left=pre; if(pre!=null) pre.right=node; pre=node; if(head==null) head=node;
        //遍历转换右子树&将根结点与右子树最小的结点链接 inOrder(node.right); }


    public TreeNode Convert2(TreeNode pRootOfTree) {
        if(pRootOfTree==null)
            return null;
        if(pRootOfTree.left==null && pRootOfTree.right==null)
            return pRootOfTree;
        //转换左子树为双链表,返回链表头结点
        TreeNode left=Convert(pRootOfTree.left);
        TreeNode p=left;
        //定位到左子树双链表的最后一个结点
        while(p!=null && p.right!=null){
            p=p.right;
        }
        //如果左子树不为空,将当前结点加到左子树链表
        if(left!=null){
            p.right=pRootOfTree;
            pRootOfTree.left=p;
        }
        //转换右子树,并返回链表头结点
        TreeNode right=Convert(pRootOfTree.right);
        //如果右子树不为空,将当前结点加到右子树链表
        if(right!=null){
            right.left=pRootOfTree;
            pRootOfTree.right=right;
        }
        //根据左子树链表是否为空,确定返回结点
        return left!=null?left:pRootOfTree;
    } }

 

题目-字符串的排列

输入一个字符串,按字典序打印出该字符串中字符的所有排列。例如输入字符串abc,则打印出由字符a,b,c所能排列出来的所有字符串abc,acb,bac,bca,cab和cba。

思路

 

 以“abc”为例,

1)将a固定住,求bc的全排列,得到abc,acb;

2)将a与b交换,bac。再求ac的全排列,得到bac,bca;

3)将a与b交换回来,再将a与c交换,cba。再求ba的全排列,得到cba,cab;

可以递归得到。

那么我们得到规律就是:若以i为开头的全排序

以当前i开始,依次与后面的元素交换,然后求i+1为开头的全排列,最后还要交换回来。

终止条件是当遍历到ch.length-1,只有一个元素,那么就遍历完了

解答

import java.util.ArrayList;
import java.util.Collections;
 
public class Solution {
    public ArrayList<String> Permutation(String str) {
        ArrayList<String> res=new ArrayList<>();
        if(str==null || str.length()==0){
            return res;
        }
        char[] ch = str.toCharArray();
        Core(res, ch, 0);
        //字典序排序
        Collections.sort(res);
        return res;
    }
     
    public void Core(ArrayList<String> res, char[] ch, int i){
        //如果这一轮交换完,且当前还没出现这种排序的ch,则添加(因为序列中可能有重复数字,所以保证不重复)
        //遍历到ch.length时,只有一个元素,递归终止
        if(i==ch.length && !res.contains(String.valueOf(ch))){
            res.add(String.valueOf(ch));
            return;
        }
        //尝试ch[i]所有可能的选择
        //遍历字符串数组,以当前i为首,依次与后面的元素交换,然后求i+1为开头的全排序
        for(int k=i; k<ch.length; k++){
            swap(ch,i,k);
            Core(res, ch, i+1);
            //相当于复原了当前递归函数中的ch,在下一轮循环中考虑该index位置上的其他可能的选项
            swap(ch,i,k);
        }
         
    }
     
    public void swap(char[] ch, int i, int j){
        char temp=ch[i];
        ch[i]=ch[j];
        ch[j]=temp;
    }
}

 

posted @ 2020-02-11 12:21  闲不住的小李  阅读(225)  评论(0编辑  收藏  举报