剑指offer|61-66题解题思路及代码(Java版)

剑指offer61到66题总览:

  1. 序列化二叉树
  2. 二叉搜索树的第k个结点
  3. 数据流中的中位数
  4. 滑动窗口的最大值
  5. 矩阵中的路径
  6. 机器人的运动范围

61、序列化二叉树

题目描述

请实现两个函数,分别用来序列化和反序列化二叉树

解题思路

序列化指的是遍历二叉树为字符串;所谓反序列化指的是依据字符串重新构造成二叉树。没有说根据什么遍历方法,所以选择前序遍历什么遍历都可以。只要序列化之后再反序列化的树是一样的。

public class Solution {
    String Serialize(TreeNode root) {
        StringBuffer str = new StringBuffer();
        if(root==null){return "#,";}
        str.append(root.val+",");//前序遍历树,先根
        str.append(Serialize(root.left));//遍历左子树,加到str的后面
        str.append(Serialize(root.right));//遍历完左子树,再遍历右子树,加到str的后面
        return str.toString();//返回遍历结果
    }

    int index = 0;
    TreeNode Deserialize(String str) {
        String[] arr = str.split(",");//如果这个split可以在外面就好一些
        TreeNode root = null;//先建一个空结点
        if(index < arr.length){
            if(arr[index].equals("#")){//如果遇到的是#,则返回空结点
                index++;//读了一个结点,index++
                return root;//返回空结点
            }else{//如果遇到的字符不是#
                root = new TreeNode(Integer.valueOf(arr[index]));//new一个新结点,并初始化val的值
                index++;//读了一个结点,index++
                root.left = Deserialize(str);//先遍历左边的字符
                root.right = Deserialize(str);//遍历完左边的字符之后,index会指向相应的右子树的根节点,遍历右边的字符
            }
        }
        return root;
    }
}

62、二叉搜索树的第k个结点

题目描述

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

解题思路

二叉搜索树中序遍历递增,二叉搜索树中数值第k小的点就是中序遍历的第k个点。

import java.util.Stack;
public class Solution {
   int index = 0;
   TreeNode KthNode(TreeNode pRoot, int k){
       if(pRoot == null){return null;}
       if(k<=0){return null;}
       Stack<TreeNode> stack = new Stack<>();
       stack.push(pRoot);//根节点入栈
       while(pRoot.left!=null){//根节点的所有左结点入栈
           pRoot = pRoot.left;
           stack.push(pRoot);
       }
       while(!stack.empty()){
           TreeNode node = stack.pop();//每出栈一个数,index+1
           index++;
           if(index < k){//当还没找到第k个数时
               if(node.right!=null){如果有右结点,则将右结点入栈,并将右结点的所有左结点入栈
                   stack.push(node.right);
                   node = node.right;
                   while(node.left!=null){//右结点的所有左结点入栈
                       node = node.left;
                       stack.push(node);
                   }
               }
           }else{//如果找到了第k个数
               pRoot = node;//将第k个数赋值给pRoot,并break
               break;
           }
       }
       if(stack.empty() && index < k){return null;}//如果是栈空了跳出的循环,且还没到第k个数,则k大于数的总结点数,返回null
       return pRoot;//如果是break跳出的循环,则返回pRoot
   }
}

63、数据流中的中位数

题目描述

如何得到一个数据流中的中位数?如果从数据流中读出奇数个数值,那么中位数就是所有数值排序之后位于中间的数值。如果从数据流中读出偶数个数值,那么中位数就是所有数值排序之后中间两个数的平均值。我们使用Insert()方法读取数据流,使用GetMedian()方法获取当前读取数据的中位数。

解题思路

一个非常直接的思想是,每次插入一个数,都找到新插入的数应该在的位置,使插入的所有数都是有序的,则取中位数的时候很方便。但这种方法的缺点时,在每次插入的时候,都要对之前插入的数据进行遍历。
参考牛客网https://www.nowcoder.com/questionTerminal/9be0172896bd43948f8a32fb954e1be1的方法,我们设置两个优先队列,也就是两个堆。我们设置一个大顶堆,一个小顶堆。而大顶堆存储插入数据中较小的那一半,小顶堆存储插入数据中较大的那一半。在后续过程中,我们会将大顶堆中的最大元素移出,移到小顶堆中;会将小顶堆中的最小元素移出,移到大顶堆中。所以大顶堆中存的数都比小顶堆中的数小。
为了平衡两个优先队列中的数,插入第一个数的时候,我们可以任意选择先加入小顶堆或者先加入大顶堆中。这个先后顺序只决定了最后我们取中位数的时候,如果我们插入了奇数个数,哪一个优先队列中存的数会多一个,我们就pop出那一个优先队列中存的数,它就是我们要的中位数。如果插入的个数为偶数个,则我们就同时pop出小顶堆和大顶堆中的数,并取平均值,得到中位数。
假设我们约定,当插入数据个数为奇数时,小顶堆的个数要多一个,则整个插入过程如下:(我们设置count,表示已经插入的数据的个数,初始值为0)

  1. 当count为偶数时,我们将新插入的数先加入大顶堆maxHeap,然后maxHeap再pop出maxHeap中最大的数,将这个最大的数加入到minHeap。这样minHeap就比maxHeap多了一个数。
  2. 当count为奇数时,minHeap比maxHeap多一个数。我们将新插入的数先加入小顶堆minHeap,然后minHeap再poop出minHeap中最小的数,将这个最小的数加入到maxHeap。这样minHeap和maxHeap的个数就一样了。

这样做,能够保证minHeap中的数总是大于maxHeap中的数,而且我们可以不关心两个堆中数的顺序,只需要关心minHeap中最小的数,和maxHeap中最大的数。同时,minHeap中存的数的个数总是比minHeap中存的数的个数多一个,或者相等。这样就非常方便取中位数了。
取中位数过程如下:

  1. 当count为偶数时,两个堆的个数是一样的,则取minHeap中最小的数,并取maxHeap中最大的数,取他们的平均值即为中位数。
  2. 当count为偶数时,minHeap比maxHeap多一个数,只需要取minHeap中最小的数,即为中位数。
import java.util.PriorityQueue;
import java.util.Comparator;
public class Solution {
    int count = 0;
    PriorityQueue<Integer> minHeap = new PriorityQueue<>();//小顶堆
    PriorityQueue<Integer> maxHeap = new PriorityQueue<>(new Comparator<Integer>(){
        @Override
        public int compare(Integer a, Integer b){//大顶堆
            return b-a;
        }
    });
    public void Insert(Integer num) {
        if(count%2==0){//当插入了偶数个数据的时候
            count++;
            maxHeap.offer(num);//先将数加入大顶堆
            minHeap.offer(maxHeap.poll());//pop出大顶堆中最大的数,并加入小顶堆
        }else{
            count++;
            minHeap.offer(num);//先将数加入小顶堆
            maxHeap.offer(minHeap.poll());//pop出小顶堆中最小的数,并加入大顶堆
        }
    }
    public Double GetMedian() {
        if(count%2 == 1){//minHeap比maxHeap多一个数,pop出minHeap的顶即可
            return (double)minHeap.peek();
        }else{//minHeap和maxHeap个数一样,取他们的顶,平均即可
            return ((double)minHeap.peek()+(double)maxHeap.peek())/2;
        }
    }
}

64、滑动窗口的最大值

题目描述

给定一个数组和滑动窗口的大小,找出所有滑动窗口里数值的最大值。例如,如果输入数组{2,3,4,2,6,2,5,1}及滑动窗口的大小3,那么一共存在6个滑动窗口,他们的最大值分别为{4,4,6,6,6,5}; 针对数组{2,3,4,2,6,2,5,1}的滑动窗口有以下6个: {[2,3,4],2,6,2,5,1}, {2,[3,4,2],6,2,5,1}, {2,3,[4,2,6],2,5,1}, {2,3,4,[2,6,2],5,1}, {2,3,4,2,[6,2,5],1}, {2,3,4,2,6,[2,5,1]}。

解题思路

参考牛客网https://www.nowcoder.com/questionTerminal/1624bc35a45c42c0bc17d17fa0cba788的方法。我们维护一个双端队列,并存储数组的下标。队头元素下标对应的数是当前滑动窗口中最大的数,队中元素下标对应的数从队头到队尾是递减的。
具体的操作如下:(队列中存储的是对应元素的下标,为了下面表述方便,我们说对应的元素)。
对num数组每个数进行遍历,将num[i]加入到双端队列queue中。

  1. 如果队列为空,则直接将num[i]对应的下标i加入到queue中,加入到队头队尾都是一样的。
  2. 如果队列非空,则从队尾开始,逐个向前检查队中下标对应的元素是否小于num[i],如果小于num[i],则全部删除,直到遇到比num[i]大的元素即停止。
    将队尾中比num[i]小的元素全部删除的原因是,加入了num[i]后,比它小的元素且不说会不会失效,有num[i]在,比num[i]小的元素都不会成为滑动窗口中的最大值,全部删除即可。
    另一方面,我们要维护的双端队列是有序的,加入num[i]前,将比num[i]小的元素全部删除,这样才能使队列有序,才能使队头元素成为我们要找的滑动窗口的最大值。
  3. 要加入i,则滑动窗口向后滑一位,我们要判断队头元素是否过期。如果队头元素对应的下标j<=i-size,即该队头元素已经出了滑动窗口,已经过期,则removeFirst,将队头移出就好。
  4. 删除了比num[i]小的结点后,并移除了过期队头,然后将num[i]对应的下标i加入到队尾。
  5. 滑动窗口更新完毕,取队头元素但不remove,加入到list就行。(前size-1个元素不用加入list)
import java.util.ArrayList;
import java.util.LinkedList;
public class Solution {
    public ArrayList<Integer> maxInWindows(int [] num, int size){
        ArrayList<Integer> list = new ArrayList<>();
        if(size <= 0){return list;}
        LinkedList<Integer> queue = new LinkedList<>();
        for(int i=0; i<num.length; i++){
            if(queue.size()==0){//队为空,不用判断是否比num[i]更小,直接加入queue
                queue.addFirst(i);//加入下标i
            }else{
                while(!queue.isEmpty() && num[queue.peekLast()]<num[i]){//从队尾开始,删除队尾中比num[i]小的数
                    queue.removeLast();
                }
                if(!queue.isEmpty() && queue.peekFirst() <= i-size){//如果队头元素过期了
                    queue.removeFirst();//移出队头元素
                }
                queue.addLast(i);//将新元素加入到队尾
            }
            if(i >= size-1){//前size-1个元素不用加入list
                list.add(num[queue.peekFirst()]);
            }
        }
        return list;
    }
}

65、矩阵中的路径

题目描述

请设计一个函数,用来判断在一个矩阵中是否存在一条包含某字符串所有字符的路径。路径可以从矩阵中的任意一个格子开始,每一步可以在矩阵中向左,向右,向上,向下移动一个格子。如果一条路径经过了矩阵中的某一个格子,则之后不能再次进入这个格子。 例如 a b c e s f c s a d e e 这样的3 X 4 矩阵中包含一条字符串"bcced"的路径,但是矩阵中不包含"abcb"路径,因为字符串的第一个字符b占据了矩阵中的第一行第二个格子之后,路径不能再次进入该格子。

解题思路

递归以及回溯。这个题是可以从matrix中任意一个点开始走的,所以与下一题的机器人路径不一样,这一题的可选方向是上下左右四向,而下一题机器人从(0,0)开始走,可选方向是右和下两向。
判断该方向是否可走,出了判断该点是否出界之外,还要判断该点是否已经存在于已经走过的路径中。一种方法可以将路径保存下来,判断是否存在于已走的路径中。这种方法可以输出可行路径。另一种方法是设置一个visited数组,判断该点是否已经访问过。这种方法能比较快地判断该点是否存在于已经走过的路径中。
如果该点上下左右四向都不通,则该点是错误走法,此时就需要回溯。我们退回走错的前一步,此题的退回即是将visited数组还原为false。

public class Solution {
    public boolean hasPath(char[] matrix, int rows, int cols, char[] str){
        if(str.length == 0 || matrix.length == 0){return false;}
        boolean visited[] = new boolean[rows*cols];//visited数组标志该位是否被访问
        for(int i=0; i<rows; i++){
            for(int j=0; j<cols; j++){//遍历matrix的每一位,从该位开始匹配字符串
                if(isHashPath(matrix, str, visited, rows, cols, i, j, 0)){
                    return true;
                }
            }
        }
        return false;
    }
    public boolean isHashPath(char[] matrix, char[] str, boolean[] visited, int rows, int cols, int i, int j, int str_index){
        if(str_index >= str.length){return true;}//str已经全部找到
        if(i<0 || i>=rows || j<0 || j>=cols || visited[i*cols+j] || matrix[i*cols+j]!=str[str_index]){
        //如果(i,j)点出了边界,或者(i,j)已经被访问,或者(i,j)点对应的字符并不是str_index对应的字符,则返回false
            return false;
        }
        visited[i*cols+j] = true;
        //如果没有出边界,也没有被访问,也是str_index对应的字符,则访问该结点,(i,j)点visited设置为true
        boolean flag =  isHashPath(matrix, str, visited, rows, cols, i, j-1, str_index+1)
                || isHashPath(matrix, str, visited, rows, cols, i, j+1, str_index+1)
                || isHashPath(matrix, str, visited, rows, cols, i-1, j,str_index+1)
                || isHashPath(matrix, str, visited, rows, cols, i+1, j, str_index+1);
        //向左,向右,向上,向下递归
        if(flag == false){//四向不通
            visited[i*cols+j] = false;//四向不通,则说明当前点走错了,回溯,将该点的visited还原为false
        }
        return flag;
    }
}

66 、机器人的运动范围

题目描述

地上有一个m行和n列的方格。一个机器人从坐标0,0的格子开始移动,每一次只能向左,右,上,下四个方向移动一格,但是不能进入行坐标和列坐标的数位之和大于k的格子。 例如,当k为18时,机器人能够进入方格(35,37),因为3+5+3+7 = 18。但是,它不能进入方格(35,38),因为3+5+3+8 = 19。请问该机器人能够达到多少个格子?

解题思路

这题不用回溯,比上一题相对简单一些。机器人从坐标(0,0)开始走,每次只需要向右和向下走即可,如果向左或向上一定是被访问过的点。每次走的时候,只需要判断向左是否能走,向右是否能走,能走就走,不能走就不走。

public class Solution {
    int count = 0;//计数器
    public int movingCount(int threshold, int rows, int cols){
        if(threshold<=0){return 0;}
        boolean visited[][] = new boolean[rows][cols];//是否访问数组
        go(threshold, rows, cols, 0, 0, visited);//从坐标(0,0)开始走
        return count;
    }
    public void go(int threshold, int rows, int cols, int i, int j, boolean[][] visited){
        if(i<0 || i>=rows || j<0 || j>=cols || visited[i][j]){return;}//如果出了边界或者改点已经访问过,则返回
        visited[i][j] = true;//如果没有出边界,且该点没有被访问过,则访问该点,将该点visited设置为true。
        count++;//访问该点,计数器count+1
        if(canGo(threshold, i, j+1)){go(threshold, rows, cols, i, j+1, visited);}//如果向右走满足threshold,则向右走
        if(canGo(threshold, i+1, j)){go(threshold, rows, cols, i+1, j, visited);}//如果向下走满足threshold,则向下走
    }
    public boolean canGo(int threshold, int i, int j){//判断该点是否满足threshold
        int sum = 0;//数位之和
        do{
            sum += i%10;//先取余,再取商
            i = i/10;
        }while(i!=0);
        do{
            sum += j%10;
            j = j/10;
        }while(j!=0);
        if(sum>threshold){return false;}//不满足threshold,返回false
        else{return true;}//满足threshold,返回true
    }
}

结尾

如果您发现我的文章有任何错误,或对我的文章有什么好的建议,请联系我!如果您喜欢我的文章,请点喜欢~*我是蓝白绛,感谢你的阅读!

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,332评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,508评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,812评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,607评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,728评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,919评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,071评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,802评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,256评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,576评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,712评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,389评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,032评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,026评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,473评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,606评论 2 350

推荐阅读更多精彩内容