劍指offer 26-30

26.二叉搜索樹與雙向鏈表

輸入一棵二叉搜索樹,將該二叉搜索樹轉(zhuǎn)換成一個排序的雙向鏈表。
要求不能創(chuàng)建任何新的結(jié)點(diǎn),只能調(diào)整樹中結(jié)點(diǎn)指針的指向。

由于二叉搜索樹已經(jīng)是排序好的了,因此我們可以采用中序遍歷的方式,對每個結(jié)點(diǎn)改變指針的方向


二叉搜索樹

我們需要用兩個輔助指針,一個pre來記錄中序遍歷情況下的上一個結(jié)點(diǎn)的位置,例如4->6->8->10->11->12->13,然后一個固定好的指針head來標(biāo)記頭結(jié)點(diǎn)的位置,例如4.
代碼如下

public class Solution {
    private TreeNode pre = null;
    private TreeNode head = null;
    public TreeNode Convert(TreeNode pRootOfTree) {
        mid_order(pRootOfTree);
        return head;
    }
    //原函數(shù)有返回值,我們在這里遞歸最好構(gòu)造一個無返回值類型的函數(shù)
    public void mid_order(TreeNode node){
        if(node==null){
            return ;
        }
        /*
        對于此處中序遍歷遞歸看不太清的可以一個一個代入進(jìn)行分析
        從下面的mid_order(node.left)一句中,其實就是不斷往左子節(jié)點(diǎn)遞歸
        遞歸到什么時候結(jié)束呢,10->6->4->null
        ok,到4.left的時候直接返回,那么此時node為4,開始執(zhí)行下面的語句
        */
        mid_order(node.left);
        //先將node的左節(jié)點(diǎn)指向pre,例如此時為4,那么4指向null
        node.left = pre;
        //下面這句話全局只有一次不生效,那就是最原始的時候pre==null
        if(pre!=null){
            pre.right = node;
        }
        //然后將pre移動到node的位置,例如從null移動到4,從4移動到6....
        //亦可以看做鏈表結(jié)點(diǎn)往后移動一個
        pre = node;
        //下面這句話全局只有一次生效,那就是不斷的遞歸到4這個初始結(jié)點(diǎn)的時候
        //相當(dāng)于找到了頭結(jié)點(diǎn)了,以后再也不會進(jìn)入這個if語句了
        if(head==null){
            head = node;
        }
        //然后進(jìn)入node.right
        //為什么這樣的順序是中序遍歷呢?
        //可以想象,在上面node等于6的時候,我們先執(zhí)行了mid_order(6.left)相關(guān)的操作
        //此時pre在4,我們執(zhí)行4,6連接的操作,然后將pre移動到6
        //等于一系列跑完了,再執(zhí)行mid_order(6.right),即此時要進(jìn)入8這個結(jié)點(diǎn)的操作了
        //后面等8跑完了,就返回到10了,10的10.right執(zhí)行的時候也會先遞歸到11
        //所以這樣一步步分析,都是順利成章到的利用遞歸實現(xiàn)中序遍歷了。
        mid_order(node.right);
    }
}

27.字符串的排列

輸入一個字符串,按字典序打印出該字符串中字符的所有排列。
例如輸入字符串a(chǎn)bc,則打印出由字符a,b,c所能排列出來的所有字符串a(chǎn)bc,acb,bac,bca,cab和cba。

這個題是個典型的回溯法,說白了就相當(dāng)于一個個試,例如“abc”的所有組合,怎么試呢,就是先把a(bǔ)放在第一個,然后再考慮后面的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轉(zhuǎn)化為字符數(shù)組
        char[] chars = str.toCharArray();
        //按chars中的字符的ASCII碼的進(jìn)行字典排序
        Arrays.sort(chars);
        //使用回溯函數(shù),創(chuàng)建兩個新的變量
        //一個是布爾數(shù)組,用以表示chars中對應(yīng)位置的字符是否已經(jīng)被使用過
        //一個是StringBuilder類,用來實時的存儲字符串
        backtracking(chars,new boolean[chars.length],new StringBuilder());
        return ret;
        
    }
    //定義一個無返回值類型的回溯函數(shù)
    public void backtracking(char[] chars,boolean[] hasUsed,StringBuilder s){
        //當(dāng)臨時變量s的長度等于字符數(shù)組的長度的時候,證明已經(jīng)組裝完成了一個字符串
        //則,此時把這個字符串加入到最后的結(jié)果中,然后返回退出本次遞歸
        if(s.length()==chars.length){
            ret.add(s.toString());
            return;
        }
        //對字符數(shù)組chars中的所有字符進(jìn)行遍歷
        for(int i=0;i<chars.length;i++){
            //如果這個字符被用過了,那么循環(huán)繼續(xù),這一步是很關(guān)鍵的
            if(hasUsed[i]){
                continue;
            }
            //這句if語句是確保不會重復(fù)
            //1.i!=0是為了chars[i-1]不為空
            //當(dāng)chars[i]==chars[i-1]時,有可能會發(fā)生重復(fù)
            /*!hasUsed[i-1]即hasUsed[i-1]為false時,可以嘗試去理解
              當(dāng)前元素等于上一個元素,而且上一個元素沒有被用過,證明上一個元素是經(jīng)歷過回溯的
              例如有兩個a,即a1,a2
              我們先組合過一次a1a2了,然后通過回溯,使得a1變成了未使用過的狀態(tài)
              假設(shè)我們此時再組合a2a1,在外在看來,實際上還是aa,這就會造成重復(fù),
              所以當(dāng)當(dāng)前元素等于上一個元素,且上一個元素是沒有被使用的狀態(tài)時,我們就跳過本次循環(huán)
              
            */
            if(i!=0&&chars[i]==chars[i-1]&&!hasUsed[i-1]){
                continue;
            }
            //然后將當(dāng)前元素加入到s中,且將當(dāng)前元素標(biāo)記為已經(jīng)使用過的狀態(tài)
            s.append(chars[i]);
            hasUsed[i]=true;
            //然后使用回溯函數(shù)進(jìn)行遞歸
            backtracking(chars,hasUsed,s);
            //然后將s的最后一個元素進(jìn)行移除,且將當(dāng)前的元素標(biāo)記為未使用
            s.deleteCharAt(s.length()-1);
            hasUsed[i]=false;
            
        }
        
    }
}

這里用到了一個hasUsed數(shù)組來標(biāo)記字符是否被使用過,而且在回溯函數(shù)中每次的參數(shù)都是chars,其實關(guān)鍵就是這個hasUsed,與之前用python寫的例如使用了b之后,將ac拼接作為新的數(shù)組來輸入到回溯函數(shù)中不同,這里的做法是,用hasUsed來標(biāo)記某個元素是否被使用過,即每次進(jìn)入遞歸的時候都需要完整的對一個長度為chars.length的chars字符數(shù)組進(jìn)行遍歷;
這么解釋起來可能有點(diǎn)抽象,用一個簡單的實例來證明,我用0代表沒被使用過,1代表被使用過,從最開始的[a0,b0,c0]來跑遞歸函數(shù);
在for循環(huán)中,當(dāng)i=0時,a被使用過了,立馬進(jìn)入了遞歸,即此時chars變成了[a1,b0,c0],可以觀察到在進(jìn)入了這一層遞歸的時候for循環(huán)依然要從下標(biāo)為0的位置開始跑,只是此時a的狀態(tài)為1了,則跳過本次循環(huán),進(jìn)入到b,b未被使用,則chars被改為了[a1,b1,c0],然后順利成章的進(jìn)入到了第二層循環(huán)的循環(huán),此時abc出爐,然后一個回溯,將c標(biāo)記為0,然后返回后b也被標(biāo)記為0,則進(jìn)入了a1狀態(tài)下的,第三個字符,起手先將第三個字符加入,變成ac,然后將c標(biāo)記為1,此時b為0,則最后為acb;
同理當(dāng)最最最外層循環(huán)的第一個字符跑完后,將a標(biāo)記為0,開始對第二個字符進(jìn)行操作,即b;
最后按遞歸的順序,所有的字符都是按照字典序添加的,如下所示
a b c
a c b
b a c
b c a
c a b
c b a

28.數(shù)組中出現(xiàn)次數(shù)超過一半的數(shù)字

數(shù)組中有一個數(shù)字出現(xiàn)的次數(shù)超過數(shù)組長度的一半,請找出這個數(shù)字。
例如輸入一個長度為9的數(shù)組{1,2,3,2,2,2,5,4,2}。
由于數(shù)字2在數(shù)組中出現(xiàn)了5次,超過數(shù)組長度的一半,因此輸出2。如果不存在則輸出0。

該題最好的解法當(dāng)然是O(n)時間復(fù)雜度的,用到兩個變量,一個變量key用來存儲所謂的“關(guān)鍵元素”,一個變量cnt用來存儲關(guān)鍵元素key的出現(xiàn)次數(shù),將key初始化為array[0],cnt初始化為1.具體算法流程為:1、先判斷cnt是否為0,如果為0,則將當(dāng)前元素賦給key,且cnt=0;否則的話判斷當(dāng)前元素與key是否相等,如果相等則cnt+1,不相等則cnt-1;2.找到了key元素之后還需要進(jìn)行檢查,因為例如12345這種情況最后的key就是5,所以還要檢查key元素在數(shù)組中出現(xiàn)的次數(shù)是否大于數(shù)組長度的一般,總結(jié)起來就是一句話,大于數(shù)組長度一半的元素一定會留下來成為key元素,但key元素并不一定是大于數(shù)組長度一般的元素,因此要進(jìn)行檢查。
代碼如下

public class Solution {
    public int MoreThanHalfNum_Solution(int [] array) {
        if(array.length==0){
            return 0;
        }
        int key = array[0];
        //從數(shù)組第一個元素開始進(jìn)行循環(huán)遍歷
        for(int i=1,cnt=1;i<array.length;i++){
            //如果cnt等于0,那么就重新計數(shù)和確定key元素
            //否則,就確定cnt數(shù)值如何變化
            if(cnt==0){
                key=array[i];
                cnt=1;
            }else{
                cnt=array[i]==key?cnt+1:cnt-1;
            }
            
        }
        //找到key元素之后,來對key元素進(jìn)行驗證
        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個數(shù)

輸入n個整數(shù),找出其中最小的K個數(shù)。例如輸入4,5,1,6,2,7,3,8這8個數(shù)字,則最小的4個數(shù)字是1,2,3,4,。

本題最簡單的思路就是用堆排來做,用一個容量為K的大根堆去維護(hù)數(shù)組,如果當(dāng)新元素加入堆之后容量超過了K,那么就拋棄堆頂元素,而java中實現(xiàn)堆的最簡單的方法就是封裝好的優(yōu)先隊列,但是注意由于隊列的性質(zhì)是先進(jìn)先出,只能彈出隊列首部的元素,因此我們需要對優(yōu)先隊列進(jìn)行降序即可。
代碼如下

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<>();
        }
        //用優(yōu)先隊列創(chuàng)建一個大根堆,注意由于隊列先進(jìn)先出的性質(zhì),所以我們要進(jìn)行降序
        //這里用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]);
            //如果優(yōu)先隊列的容量大于K,則彈出隊列首部的元素
            if(maxHeap.size()>k){
                maxHeap.poll();
            }
        }
        return new ArrayList<>(maxHeap);
    }
}

30.連續(xù)子數(shù)組的最大和

HZ偶爾會拿些專業(yè)問題來忽悠那些非計算機(jī)專業(yè)的同學(xué)。
今天測試組開完會后,他又發(fā)話了:在古老的一維模式識別中,常常需要計算連續(xù)子向量的最大和,當(dāng)向量全為正數(shù)的時候,問題很好解決。
但是,如果向量中包含負(fù)數(shù),是否應(yīng)該包含某個負(fù)數(shù),并期望旁邊的正數(shù)會彌補(bǔ)它呢?
例如:{6,-3,-2,7,-15,1,2,2},連續(xù)子向量的最大和為8(從第0個開始,到第3個為止)。
給一個數(shù)組,返回它的最大連續(xù)子序列的和,你會不會被他忽悠???(子向量的長度至少是1)

典型的入門DP問題,用一個dp數(shù)組來存儲以array中每個元素為結(jié)尾的可能的最大連續(xù)子數(shù)組和,試想,當(dāng)我處理第i項時,已經(jīng)存儲了前面i-1個元素的最大連續(xù)子數(shù)組和,如果前面這個和即dp[i-1]已經(jīng)小于0了,那我要前面的何用?以我array[i]結(jié)尾的如果把前面的加上豈不是越加越小?所以此時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中每個元素為結(jié)尾能夠得到的連續(xù)子數(shù)組的最大值的一個數(shù)組,然后求出這個數(shù)組的最大值,即可.
代碼如下

public class Solution {
    public int FindGreatestSumOfSubArray(int[] array) {
        if(array.length==0){
            return 0;
        }
        //先復(fù)制一份原數(shù)組
        int[] dp = array.clone();
        //動態(tài)規(guī)劃過程
        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數(shù)組的最大值
        for(int num:dp){
            max = Math.max(max,num);
        }
        return max;
    }
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容