剑指offer 26-30

26.二叉搜索树与双向链表

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

由于二叉搜索树已经是排序好的了,因此我们可以采用中序遍历的方式,对每个结点改变指针的方向


二叉搜索树

我们需要用两个辅助指针,一个pre来记录中序遍历情况下的上一个结点的位置,例如4->6->8->10->11->12->13,然后一个固定好的指针head来标记头结点的位置,例如4.
代码如下

public class Solution {
    private TreeNode pre = null;
    private TreeNode head = null;
    public TreeNode Convert(TreeNode pRootOfTree) {
        mid_order(pRootOfTree);
        return head;
    }
    //原函数有返回值,我们在这里递归最好构造一个无返回值类型的函数
    public void mid_order(TreeNode node){
        if(node==null){
            return ;
        }
        /*
        对于此处中序遍历递归看不太清的可以一个一个代入进行分析
        从下面的mid_order(node.left)一句中,其实就是不断往左子节点递归
        递归到什么时候结束呢,10->6->4->null
        ok,到4.left的时候直接返回,那么此时node为4,开始执行下面的语句
        */
        mid_order(node.left);
        //先将node的左节点指向pre,例如此时为4,那么4指向null
        node.left = pre;
        //下面这句话全局只有一次不生效,那就是最原始的时候pre==null
        if(pre!=null){
            pre.right = node;
        }
        //然后将pre移动到node的位置,例如从null移动到4,从4移动到6....
        //亦可以看做链表结点往后移动一个
        pre = node;
        //下面这句话全局只有一次生效,那就是不断的递归到4这个初始结点的时候
        //相当于找到了头结点了,以后再也不会进入这个if语句了
        if(head==null){
            head = node;
        }
        //然后进入node.right
        //为什么这样的顺序是中序遍历呢?
        //可以想象,在上面node等于6的时候,我们先执行了mid_order(6.left)相关的操作
        //此时pre在4,我们执行4,6连接的操作,然后将pre移动到6
        //等于一系列跑完了,再执行mid_order(6.right),即此时要进入8这个结点的操作了
        //后面等8跑完了,就返回到10了,10的10.right执行的时候也会先递归到11
        //所以这样一步步分析,都是顺利成章到的利用递归实现中序遍历了。
        mid_order(node.right);
    }
}

27.字符串的排列

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

这个题是个典型的回溯法,说白了就相当于一个个试,例如“abc”的所有组合,怎么试呢,就是先把a放在第一个,然后再考虑后面的bc,后面的b、c有bc和cb两种,拼接在a后面,就是abc和acb两种了。
代码如下

import java.util.ArrayList;
import java.util.Arrays;
public class Solution {
    //定义一个存储String的ArrayList
    private ArrayList<String> ret = new ArrayList<>();
    public ArrayList<String> Permutation(String str) {
        if(str.length()==0){
            return ret;
        }
        //将str转化为字符数组
        char[] chars = str.toCharArray();
        //按chars中的字符的ASCII码的进行字典排序
        Arrays.sort(chars);
        //使用回溯函数,创建两个新的变量
        //一个是布尔数组,用以表示chars中对应位置的字符是否已经被使用过
        //一个是StringBuilder类,用来实时的存储字符串
        backtracking(chars,new boolean[chars.length],new StringBuilder());
        return ret;
        
    }
    //定义一个无返回值类型的回溯函数
    public void backtracking(char[] chars,boolean[] hasUsed,StringBuilder s){
        //当临时变量s的长度等于字符数组的长度的时候,证明已经组装完成了一个字符串
        //则,此时把这个字符串加入到最后的结果中,然后返回退出本次递归
        if(s.length()==chars.length){
            ret.add(s.toString());
            return;
        }
        //对字符数组chars中的所有字符进行遍历
        for(int i=0;i<chars.length;i++){
            //如果这个字符被用过了,那么循环继续,这一步是很关键的
            if(hasUsed[i]){
                continue;
            }
            //这句if语句是确保不会重复
            //1.i!=0是为了chars[i-1]不为空
            //当chars[i]==chars[i-1]时,有可能会发生重复
            /*!hasUsed[i-1]即hasUsed[i-1]为false时,可以尝试去理解
              当前元素等于上一个元素,而且上一个元素没有被用过,证明上一个元素是经历过回溯的
              例如有两个a,即a1,a2
              我们先组合过一次a1a2了,然后通过回溯,使得a1变成了未使用过的状态
              假设我们此时再组合a2a1,在外在看来,实际上还是aa,这就会造成重复,
              所以当当前元素等于上一个元素,且上一个元素是没有被使用的状态时,我们就跳过本次循环
              
            */
            if(i!=0&&chars[i]==chars[i-1]&&!hasUsed[i-1]){
                continue;
            }
            //然后将当前元素加入到s中,且将当前元素标记为已经使用过的状态
            s.append(chars[i]);
            hasUsed[i]=true;
            //然后使用回溯函数进行递归
            backtracking(chars,hasUsed,s);
            //然后将s的最后一个元素进行移除,且将当前的元素标记为未使用
            s.deleteCharAt(s.length()-1);
            hasUsed[i]=false;
            
        }
        
    }
}

这里用到了一个hasUsed数组来标记字符是否被使用过,而且在回溯函数中每次的参数都是chars,其实关键就是这个hasUsed,与之前用python写的例如使用了b之后,将ac拼接作为新的数组来输入到回溯函数中不同,这里的做法是,用hasUsed来标记某个元素是否被使用过,即每次进入递归的时候都需要完整的对一个长度为chars.length的chars字符数组进行遍历;
这么解释起来可能有点抽象,用一个简单的实例来证明,我用0代表没被使用过,1代表被使用过,从最开始的[a0,b0,c0]来跑递归函数;
在for循环中,当i=0时,a被使用过了,立马进入了递归,即此时chars变成了[a1,b0,c0],可以观察到在进入了这一层递归的时候for循环依然要从下标为0的位置开始跑,只是此时a的状态为1了,则跳过本次循环,进入到b,b未被使用,则chars被改为了[a1,b1,c0],然后顺利成章的进入到了第二层循环的循环,此时abc出炉,然后一个回溯,将c标记为0,然后返回后b也被标记为0,则进入了a1状态下的,第三个字符,起手先将第三个字符加入,变成ac,然后将c标记为1,此时b为0,则最后为acb;
同理当最最最外层循环的第一个字符跑完后,将a标记为0,开始对第二个字符进行操作,即b;
最后按递归的顺序,所有的字符都是按照字典序添加的,如下所示
a b c
a c b
b a c
b c a
c a b
c b a

28.数组中出现次数超过一半的数字

数组中有一个数字出现的次数超过数组长度的一半,请找出这个数字。
例如输入一个长度为9的数组{1,2,3,2,2,2,5,4,2}。
由于数字2在数组中出现了5次,超过数组长度的一半,因此输出2。如果不存在则输出0。

该题最好的解法当然是O(n)时间复杂度的,用到两个变量,一个变量key用来存储所谓的“关键元素”,一个变量cnt用来存储关键元素key的出现次数,将key初始化为array[0],cnt初始化为1.具体算法流程为:1、先判断cnt是否为0,如果为0,则将当前元素赋给key,且cnt=0;否则的话判断当前元素与key是否相等,如果相等则cnt+1,不相等则cnt-1;2.找到了key元素之后还需要进行检查,因为例如12345这种情况最后的key就是5,所以还要检查key元素在数组中出现的次数是否大于数组长度的一般,总结起来就是一句话,大于数组长度一半的元素一定会留下来成为key元素,但key元素并不一定是大于数组长度一般的元素,因此要进行检查。
代码如下

public class Solution {
    public int MoreThanHalfNum_Solution(int [] array) {
        if(array.length==0){
            return 0;
        }
        int key = array[0];
        //从数组第一个元素开始进行循环遍历
        for(int i=1,cnt=1;i<array.length;i++){
            //如果cnt等于0,那么就重新计数和确定key元素
            //否则,就确定cnt数值如何变化
            if(cnt==0){
                key=array[i];
                cnt=1;
            }else{
                cnt=array[i]==key?cnt+1:cnt-1;
            }
            
        }
        //找到key元素之后,来对key元素进行验证
        int cnt=0;
        for(int j=0;j<array.length;j++){
            cnt=array[j]==key?cnt+1:cnt;
        }
        return cnt>array.length/2?key:0;
    }
}

29.最小的K个数

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

本题最简单的思路就是用堆排来做,用一个容量为K的大根堆去维护数组,如果当新元素加入堆之后容量超过了K,那么就抛弃堆顶元素,而java中实现堆的最简单的方法就是封装好的优先队列,但是注意由于队列的性质是先进先出,只能弹出队列首部的元素,因此我们需要对优先队列进行降序即可。
代码如下

import java.util.PriorityQueue;
import java.util.ArrayList;

public class Solution {
    public ArrayList<Integer> GetLeastNumbers_Solution(int [] input, int k) {
        //如果k<0或者input为空或者k大于input的长度,那么返回一个空的ArrayList
        if(k<0||input.length==0||k>input.length){
            return new ArrayList<>();
        }
        //用优先队列创建一个大根堆,注意由于队列先进先出的性质,所以我们要进行降序
        //这里用o1,o2指代两个元素,用o2-o1代表降序
        PriorityQueue<Integer> maxHeap = new PriorityQueue<>((o1,o2)->o2-o1);
        for(int i=0;i<input.length;i++){
            maxHeap.add(input[i]);
            //如果优先队列的容量大于K,则弹出队列首部的元素
            if(maxHeap.size()>k){
                maxHeap.poll();
            }
        }
        return new ArrayList<>(maxHeap);
    }
}

30.连续子数组的最大和

HZ偶尔会拿些专业问题来忽悠那些非计算机专业的同学。
今天测试组开完会后,他又发话了:在古老的一维模式识别中,常常需要计算连续子向量的最大和,当向量全为正数的时候,问题很好解决。
但是,如果向量中包含负数,是否应该包含某个负数,并期望旁边的正数会弥补它呢?
例如:{6,-3,-2,7,-15,1,2,2},连续子向量的最大和为8(从第0个开始,到第3个为止)。
给一个数组,返回它的最大连续子序列的和,你会不会被他忽悠住?(子向量的长度至少是1)

典型的入门DP问题,用一个dp数组来存储以array中每个元素为结尾的可能的最大连续子数组和,试想,当我处理第i项时,已经存储了前面i-1个元素的最大连续子数组和,如果前面这个和即dp[i-1]已经小于0了,那我要前面的何用?以我array[i]结尾的如果把前面的加上岂不是越加越小?所以此时dp[i]就等于array[i],如果前面的大于或者等于0,那么把前面的加起来,岂不是美滋滋?所以此时dp[i]=dp[i-1]+array[i];
dp方程如下:

从第1项开始
dp[i] = dp[i-1]>0?dp[i-1]+array[i]:array[i];

求出了以array中每个元素为结尾能够得到的连续子数组的最大值的一个数组,然后求出这个数组的最大值,即可.
代码如下

public class Solution {
    public int FindGreatestSumOfSubArray(int[] array) {
        if(array.length==0){
            return 0;
        }
        //先复制一份原数组
        int[] dp = array.clone();
        //动态规划过程
        for(int i = 1;i<array.length;i++){
            dp[i] = dp[i-1]>0?dp[i-1]+array[i]:array[i];
        }
        int max = dp[0];
        //求出dp数组的最大值
        for(int num:dp){
            max = Math.max(max,num);
        }
        return max;
    }
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 221,695评论 6 515
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 94,569评论 3 399
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 168,130评论 0 360
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,648评论 1 297
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,655评论 6 397
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 52,268评论 1 309
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,835评论 3 421
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,740评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 46,286评论 1 318
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,375评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,505评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 36,185评论 5 350
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,873评论 3 333
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,357评论 0 24
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,466评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,921评论 3 376
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,515评论 2 359

推荐阅读更多精彩内容