1. 程式人生 > >【劍指offer】1-10題:C++和Java版

【劍指offer】1-10題:C++和Java版

劍指offer  面試題1:賦值運算子函式 題目:如下為型別CMyString 的宣告,請為該型別新增賦值符函式。

class CmyString
{
public:
    CmyString(char* pData = nullptr);
    CmyString(const CMyString& str);//建構函式中傳值引數為常量引用
    ~CmyString(void);
private:
    char* m_pData;
};

寫出的程式碼關注如下幾點: 1、是否把返回值的型別宣告為該型別的引用,並在函式結束前返回例項自身的引用(*this)。 只有返回一個引用,才可以允許連續賦值。否則,如果函式的返回值是void,則應用該賦值運算子 將不能進行連續賦值。 2、是否把傳入的引數的型別宣告為常量引用。如果傳入的引數不是引用而是例項,那麼從形參 到實參會呼叫一次複製建構函式。把引數宣告為引用可以避免這樣的無謂消耗,能提高程式碼的 效率。同時,在賦值運算子函式內不會改變傳入的例項的狀態,因此應該為傳入的引用引數 加上const關鍵字。 3、是否釋放例項自身已有的記憶體。如果我們忘記在分配新記憶體之前釋放自身已有的空間, 則程式將出現記憶體洩露。 4、判斷傳入的引數和當前的例項(*this)是不是同一個。如果是同一個,則不進行賦值操作, 直接返回。當*this和傳入的引數是同一個例項時,一旦釋放了自身的記憶體,傳入的引數的記憶體 也同時被釋放了,因此再也找不到需要賦值的內容了。 在賦值運算子函式中實現異常安全性,我們有兩種方法。一種簡單的辦法是我們先用new分配 新內容,再用delete釋放已有的內容。這樣只在分配內容成功之後再釋放原來的內容,也就是 當分配記憶體失敗時我們能確保CMyString的例項不會被修改。還有一種更好的辦法,即先建立 一個臨時例項,再交換臨時例項和原來的例項。 C++版

CMyString& CMyString::operator =(const CmyString &str)
{
    if(this != &str)
    {
        CMyString strTemp(str);//建立一個臨時例項strTemp
        
        char* pTemp = strTemp.m_pData;//把strTemp.m_pData和
        strTemp.m_pData = m_pData;//例項自身的m_pData進行交換
        m_pData = pTemp;
    }
    
    return *this;
}

我們先建立一個臨時例項strTemp,接著把strTemp.m_pData和 例項自身的m_pData進行交換。由於strTemp是一個區域性變數,但程式 執行到if的外面時也就出了該變數的作用域,就會自動呼叫strTemp的 解構函式,把strTemp.m_pData所指向的記憶體釋放掉。由於strTemp.m_pData 指向的記憶體就是例項之前m_pData的記憶體,這就相當於自動呼叫解構函式 釋放例項的記憶體。

面試題2:實現Singleton模式 題目:設計一個類,我們只能生成該類的一個例項。 C#版 可行的解法:加同步鎖前後兩次判斷例項是否已經存在 我們只是在例項還沒有建立之前需要加鎖操作,以保證只有一個執行緒創建出例項。 而當例項已經建立之後,我們已經不需要再執行加鎖操作了。

public sealed class Singleton3 {
    private Singleton3(){
    }
    private static object syncObj = new object();
    private static Singleton3 instance = null;
    public static Singleton3 Instance{
        get{
            if(instance == null){
                lock(syncObj){
                    if(instance == null){
                        instance = new Singleton3();
                    }
                }
            return instance;
        }
    }    
}

Singleton3中只有當instance為null既沒有建立時,需要加鎖操作。當 instance已經創建出來之後,則無須加鎖。 Singleton3用加鎖機制來確保在多執行緒環境下只建立一個例項,並且 用兩個if判斷來提高效率。這樣的程式碼實現起來比較複雜,容易出錯, 我們還有更加優秀的解法。 強烈推薦的解法一:利用靜態建構函式 C#的語法中有一個函式能夠確保只調用一次,那就是靜態建構函式,我們 可以利用C#的這個特性實現單例模式。

public sealed class Singleton4{
    private Singleton4(){
    }
    private static Singleton4 instance = new Singleton4();
    public static Singleton4 Instance{
        get{
            return instance;
        }
    }
}

我們在初始化靜態變數instance的時候建立一個例項。由於C#是在呼叫靜態構造 函式時初始化靜態變數,.NET執行時能夠確保只調用一次靜態建構函式,這樣我們 就能夠保證只初始化一次instance。 強烈推薦的解法二: 實現按需建立例項 最後一個實現Singleton5則很好解決了Singleton4中的例項建立時機過早的問題。

public sealed class Singleton5{
    Singleton5(){
    }
    public static Singleton5 Instance{
        get{
            return Nested.instance;
        }
    }
    class Nested{
        static Nested(){
        }
        internal static readonly Singleton5 instance = new Singleton5(); 
    }
}

在上述SIngleton5的程式碼中,我們在內部定義了一個私有型別Nested。當第一次 用到這個巢狀型別的時候,會呼叫靜態建構函式建立Singleton5的例項instance。 型別Nested只在屬性Singleton5.Instance中被用到,由於其私有屬性,他人無法 使用Nested型別。因此,當我們第一次試圖通過屬性Singleton5.Instance得到 Singleton5的例項時,會自動呼叫Nested的靜態建構函式建立例項instance。如果 我們不呼叫屬性Singleton5.Instance,就不會觸發.NET執行呼叫Nested,也不會 建立例項,這樣就真證做到了按需建立。 解法比較: 在Singleton3中,我們通過兩次判斷一次加鎖確保在多執行緒環境中能高效地工作。 Singleton4中利用C#的靜態建構函式的特性,確保只建立一個例項。在Singleton5 中利用私有巢狀型別的特性,做到只在真正需要的時候才會建立例項,提高空間使用效率。 Java版 單例模式的特點 單例類只能有一個例項; 單例類必須自己建立自己的唯一例項; 單例類必須給所有其他物件提供這一例項。 單例模式的應用 在計算機系統中,執行緒池、緩衝、日誌物件、對話方塊、印表機、顯示卡的驅動 程式物件常被設計成單例; 這些應用多少具有資源管理器的功能。每臺計算機可以有若干個印表機,但 只能有一個Printer Spooler,以避免兩個列印作業同時輸出到印表機中。 每臺計算機可以有若干通訊埠,系統應集中管理這些通訊埠,以避免一個 通訊埠同時被兩個請求同時呼叫。總之,選擇單例模式就是為了避免不一致 狀態。 單例模式的Java程式碼 單例模式分為懶漢式(需要才去建立物件)和餓漢式(建立類的例項時就去創 建物件)。 餓漢式 屬性例項化物件 //餓漢模式:執行緒安全,耗費資源。

public class HugerSingletonTest {
    //該物件的引用不可修改
    private static HugerSingletonTest Instance = new HugerSingletonTest();
    
    public static HugerSingletonTest getInstance() {
        return Instance;
    }
    private HugerSingletonTest() {
        
    }
}

在靜態程式碼塊例項物件

public class Singleton {
    private static Singleton Instance;
    
    static {
        Instance = new Singleton();
    }
    public static Singleton getInstance() {
        return Instance;
    }
    private Singleton() {
        
    }
} 

分析:餓漢式單例模式只要呼叫了該類,就會例項化一個物件,但有時我們並只需要呼叫該類的 一個方法,而不需要例項化一個物件,所以餓漢式是比較消耗資源的。 懶漢式 非執行緒安全

public class Singleton {
    private static Singleton Instance;
    
    public static Singleton getInstance() {
        if (null == Instance) {
            Instance = new Singleton();
        }
        return Instance;
    }
    private Singleton() {
        
    }
}

分析:如果有兩個執行緒同時呼叫getInstance()方法,則會建立兩個例項化物件。 所以是非執行緒安全的。 執行緒安全:給方法加鎖

public class Singleton {
    private static Singleton Instance;
    public synchronized static Singleton getInstance() {
        if (null == Instance) {
            Instance = new Singleton();
        }
        return Instance;
    }
    private Singleton() {
        
    }
} 

分析:如果有多個執行緒呼叫getInstance()方法,當一個執行緒獲取該方法,而其它執行緒必須等待, 消耗資源。 執行緒安全:雙重檢查鎖(同步程式碼塊)

public class Singleton {
    private static Singleton Instance;
    public synchronized static Singleton getInstance() {
        if (null == Instance) {//非空物件就不需要同步了
            synchronized (Singleton.class){//空物件的執行緒然後進入同步程式碼塊
                if (null == Instace){
                    Instance = new Singleton();
                }
            }
            
        }
        return Instance;
    }
    private Singleton() {
        
    }
}

分析:雙重檢查鎖,第一次檢查是確保之前是一個空物件,而非空物件就不需要同步了,空物件的 執行緒然後進入同步程式碼塊,如果不加第二次空物件檢查,兩個執行緒同時獲取同步程式碼,一個執行緒 進入同步程式碼塊,另一個執行緒就會等待,而這兩個執行緒就會建立兩個例項化物件,所以需要線上程 進入同步程式碼塊後再次進行空物件檢查,才能確保只建立一個例項化物件。 執行緒安全:靜態內部類

public class Singleton {
    private static class SingletonHodler {
        private static Singleton Instance = new Singleton9);
    }
    public sychronized static Singleton getInstacne() {
        return SingletonHodler.Instance;
    }
    private Singleton() {
        
    }
} 

分析: 利用靜態內部類,某個執行緒呼叫該方法時會建立一個例項化物件。 執行緒安全:列舉

enum SingletonTest {
    INSTANCE;
    public void whateverMethod() {
        
    }
}

分析:列舉的方式,它不僅能避免多執行緒同步問題,而且還能防止反序列化重新建立新的物件, 但是在列舉中的其他任何方法的執行緒安全由程式設計師自己負責。還有防止上面的通過反射機制 呼叫私用構造器。 指令重排序 懶漢式的雙重檢查版本的單例模式,不一定是執行緒安全的,因為在JVM的編譯過程中會存在 指令重排序的問題  其實建立一個物件,往往包含三個過程。 對於singleton = new Singleton(),這不是一個原子操作,在JVM中包含的三個過程。 1>給singleton分配記憶體 2>呼叫singleton的建構函式來初始化成員變數,形成例項 3>將singleton物件指向分配的記憶體空間(執行完這步singleton才是非null了) 把singleton宣告成volatile,改進後的懶漢式執行緒(雙重檢查鎖)的程式碼如下:

public class Singleton {
    //volatile的作用是:保證可見性、禁止指令重排序,但不能保證原子性
    private volatile static Singleton Instance;
    public synchronized static Singleton getInstance() {
        if (null == Instance) {//非空物件就不需要同步了
            synchronized (Singleton.class){//空物件的執行緒然後進入同步程式碼塊
                if (null == Instace){
                    Instance = new Singleton();
                }
            }            
        }
        return Instance;
    }
    private Singleton() {        
    }
}

單例模式在JDK8原始碼中的使用 當然JDK原始碼中使用了大量的設計模式,Runtime類 原始碼如下: //餓漢式單例模式

public class Runtime {
    private static Runtime currentRuntime = new Runtime();
    
    public static Runtime getRuntime() {
        return currentRuntime;
    }
    private Runtime() {
    }
    //省略多行
}

面試題3:陣列中重複的數字 C++版 題目一:找出陣列中的重複的數字。 在一個長度為n的數組裡的所有數字都在0~n-1的範圍內。陣列中某些數字是重複的,但不知道有幾個數字重複了, 也不知道每個數字重複了幾次。請找出陣列中任意一個重複的數字。例如,如果輸入長度為7的陣列{2,3,1,0,2,5,3}, 那麼對應的輸出是重複的數字2或者3。 簡單的方法是先把輸入的陣列排序。從排序的陣列中找出重複的數字,排序一個長度為n的陣列需要O(nlogn)的時間。 利用雜湊表來解決這個問題。從頭到尾按順序掃描陣列的每個數字,每掃描到一個數字的時候,都可以用O(1)的時間 來判斷雜湊表裡是否已經包含了該數字。如果雜湊表裡還沒有這個數字,就把它加入雜湊表。如果雜湊表裡已經存在 該數字,就找到一個重複的數字。以一個大小為O(n)的雜湊表為代價,這個演算法的時間複雜度為O(n)。 我們注意到陣列中的數字都在0~n-1的範圍內。如果這個陣列中沒有重複的數字,那麼當陣列排序之後數字i將出現在 下標為i的位置。由於陣列中有重複的數字,有些位置可能存在對個數字,同時有些位置可能沒有數字。 現在讓我們重排這個陣列。從頭到尾依次掃描這個陣列中的每個數字。當掃描到下標為i的數字時,首先比較這個數字 (用m表示)是不是等於i。如果是,則接著掃描下一個數字;如果不是,則再拿它和第m個數字進行比較。如果它和第m個 數字相等,就找到了一個重複的數字(該數字在下標為i和m的位置都出現了);如果它和第m個數字不相等,就把第i數字 和第m個數字交換,把m放到屬於它的位置。接下來再重複這個比較、交換的過程,直到我們發現一個重複的數字。

bool duplicate(int numbers[],int length,int* duplication){
    if(numbers == nullptr || length <=0){
        return false;
    }
    for(int i = 0; i < length; ++i){
        if(numbers[i]<0 || numbers[i]>length-1)
            return false;
    }
    for(int i = 0; i < length; ++i){
        while(number[i] != i){
            if(numbers[i] == numbers[numbers[i]]){
                *duplication = numbers[i];
                return true;
            }
            
            //swap numbers[i] and numbers[numbers[i]]
            int temp = numbers[i];
            numbers[i] = numbers[temp];
            numbers[temp] = temp;
        }
    }
    return false;
}

在上述程式碼中,找到的重複數字通過引數duplication傳給函式的呼叫者,而函式的返回值表示陣列中 是否有重複的數字。當輸入的陣列中存在重複的數字時,返回true;否則返回false。 程式碼中儘管有一個兩重迴圈,但每個數字最多隻要交換兩次就能找到屬於它自己的位置,因此總的時間 複雜度是O(n)。另外,所有的操作步驟都是在輸入陣列上進行的,不需要額外分配記憶體,因此空間複雜度為O(1)。

Java版 引數宣告 numbers:輸入的整數陣列 length:陣列的長度 duplication:輸出任意一個重複的數字。使用duplication[0]表示

public boolean duplicate(int numbers[], int length, int [] duplication){
}

輸出 1.如果存在重複的數字,返回值為true。並使用duplication{0}返回一個重複的數字。 2.如果不存在重複的數字,返回false。 思路1 1.遍歷陣列,採用hashmap存放每個元素,其中元素作為key儲存,value為0. 2.當前遍歷元素插入hashmap時,先檢查hashmap中是否已經存在同樣的key。 3.若存在,記錄下該值,返回true;若不存在,存入map中,繼續遍歷,直到陣列結束,返回false。

public boolean duplicate(int numbers[],int length,int [] duplication){
    boolean flag = false;
    if(numbers == null || length == 0){
        return flag;
    }
    HashMap<Integer,Integer> map = new HashMap<Integer,Integer>();
    for (int num : numbers){
        if(map.containsKey(num)){
            flag = true;
            duplication[0] = num;
            break;
        }
        map.put(num,0);
    }
    return flag;
}

思路2 1.注意到輸入是長度為n的陣列,所有數字都在0到n-1的範圍內。可以利用原陣列的特點設定標誌。 2.當一個數字被訪問到後,在其對應的位數上面進行-length的操作。 3.當下次訪問到重複數值時,只需要檢查對應的位數上面的值是否小於0,就可以判斷該數字是否是重複出現。 4.該方法缺點是改變了原陣列。

public boolean duplicate(int numbers[],int length,int [] duplication){
    boolean flag = false;
    if (numbers == null || length == 0) {
        return flag;
    }
    for (int i = 0; i < length; i++) {
        int index = numbers[i];
        if (index < 0){
            index += length;
        }
        if (numbers[index] < 0){
            //index<0時有一個還原+length的操作,返回的就是原來重複的數字
            duplication[0] = index;
            falg = true;
            break;
        }
        numbers[index] -= length;
    }
    return flag;
}

思路3 1.利用字串拼接每個元素 2.判斷字串中同一個元素的子串,是否出現在了兩個位置 3.StringBuffer與String的不同之處在於,String每次拼接都會返回一個新的物件,而StringBuffer不會。

public boolean duplicate(int numbers[],int length,int [] duplication){
    StringBuffer sb = new StringBuffer();
    for(int i = 0; i < length; i++){
        sb.append(numbers[i] + "");
    }
    for(int j = 0; j < length; j++){
        //判斷numbers[j]+""首次出現下標和最後一次出現的下標是否相等
        if(sb.indexOf(numbers[j]+"")!= sb.lastIndexOf(numbers[j]+"")){
            duplication[0] = numbers[j];
            return true;
        }
    }
    return false;
}

面試題4:二維陣列中的查詢 題目描述 在一個二維陣列中,每一行都按照從左到右遞增的順序排序,每一列都按照從上到下遞增的順序排序。 請完成一個函式,輸出這樣的一個二維陣列和一個整數,判斷陣列中是否含有該整數。 C++版 例如下面的二維陣列就是每行、每列都遞增排序。如果在這個陣列中查詢數字7,則返回true; 如果查詢5,由於陣列不含有該數字,則返回false。  1  2  8  9  2  4  9  12  4  7  10 15  6  8  11 15  分析這個問題,把二維陣列畫成矩形,然後從陣列中選取一個數字,分3中情況來分析查詢的過程。  當陣列中選取的數字剛好和要查詢的數字相等時,就結束查詢過程。如果選取的數字小於要查詢的數字,  那麼根據陣列排序的規則,要查詢的數字應該在當前選取位置的右邊或者下邊。同樣,如果選取的數字  大於要查詢的數字,那麼要查詢的數字應該在當前選取位置的上邊或者左邊。  在上面的分析中,由於要查詢的數字相對於當前選取的位置有可能在兩個區域中出現,而且這兩個區域還有重疊。  之所以遇到這樣的難題,是因為我們在二維陣列的中間來和要查詢的數字進行比較,就導致下一次要查詢的是兩個  相互重疊的區域。如果我們從陣列的一個角上選取數字來和要查詢的數字進行比較,那麼情況會變得簡單。  總結查詢過程,發現以下規律:首先選取陣列中右上角的數字。如果該數字等於要查詢的數字,則查詢過程結束;  如果該數字大於要查詢的數字,則剔除這個數字所在的列; 如果該數字小於要查詢的數字,則剔除這個數字所在的行;  也就是說,如果要查詢的數字不在陣列的右上角,則每一次都在陣列的查詢範圍中剔除一行或者一列,這樣每一步都可以  縮小查詢的範圍,直到找到要查詢的數字,或者查詢範圍為空。

 bool Find(int* matrix, int rows, int columns,int number) {
     bool found = false;
     if (matrix != nullptr && rows > 0 && columns > 0){//指標matrix非空指標且行、列數大於0
        //定義第一行
        int row = 0;
        int column = columns - 1;
        while (row < rows && columns > 0) {
            if (matrix[row * columns + column] == number) {//陣列中右上角的元素等於要查詢的數字
                found = true;
                break;
            }
            else if (matrix[row * columns + column] > number) {//如果該數字大於要查詢的數字
                //則剔除這個數字所在的列;
                --column;
            else//如果該數字小於要查詢的數字
                //則剔除這個數字所在的行;
                ++row;
        }
     }
     return found;
 }

Java版

public class Solution{
    public boolean Find(int target, int [][] array) {
        int i = 0;
        //得到行數
        int len = array.length - 1;
        while ((len >= 0) && (i < array[0].length)) {
            
            if (array[len][i] > target) {//如果當前行陣列元素大於目標值
                //則行數減一
                len--;
            }
            else if (array[len][i] < target) {//如果當前行陣列元素小於目標值
                //向右走
                i++;
            }
            else {//如果當前行陣列元素等於目標值
                return true;//則返回true,陣列中含有該整數
            }
        }//while迴圈結束,未找到目標值
        return false;//返回false,陣列中不含有該整數
    }
}

面試題5:替換空格 請實現一個函式,將一個字串中的空格替換成"%20"。 例如,當字串為We Are Happy,則經過替換之後的字串為We%20Are%20Happy。 C++版 看到這個題目,首先應該想到的是原來一個空格字元,替換之後程式設計'%'、'2'和'0'這3個字元,因此字串會變長。 如果是在原來的字串上進行替換,就有可能覆蓋修改在該字串後面的記憶體。如果是建立新的字串並在新的 字串上進行替換,那麼我們可以自己分配足夠多的記憶體。 時間複雜度為O(n*n)的解法,不足以拿到offer。 從尾到頭掃描字串,每次碰到空格字元的時候進行替換。由於是把1個字元替換成3個字元,我們必須要把空格後面 所有的字元都後移2位元組,否則就有兩個字元被覆蓋了。 假設字串的長度是n。對每個空格字元,需要移動後面O(n)個字元,因此對於含有O(n)個空格字元的字串而言,總的 時間效率是O(n*n)。 我們換一種思路,把從前向後替換改成從後向前替換。 時間複雜度為O(n)的解法,搞定Offer就靠它了 先遍歷一次字串,統計出字串中空格的總數,由此計算出替換之後的字串的總長度。 我們從字串的後面開始複製和替換。首先準備兩個指標:P1和P2。(a)P1指向原始字串的末尾,而P2指向替換之後 的字串的末尾。(b)接下來我們向前移動指標P1,逐個把它指向的字串複製到P2指向的位置,直到碰到第一個空格 為止。(c)碰到第一個空格之後,把P1向前移動1格,在P2之前插入字串"%20"。由於"%20"的長度為3,同時也要把P2 向前移動3格。(d)接著向前複製,直到碰到第二個空格,(e)和上次一樣,我們再把P1向前移動1格,並把P2向前移動3格插入 "%20"。此時P1和P2指向同一位置,表明所有空格都已經替換完畢。 從上面的分析中,可以看出,所有的字元都只複製(移動)一次,一次這個演算法的時間效率是O(n),比第一個思路要快。

/*length為字元陣列String的總容量*/
void ReplaceBlank(char string[], int length){
    if(string = nullstr || length <= 0)
        return;
    /*originalLength 為字串的實際長度*/
    int originalLength = 0;
    int numberOfBlank = 0;
    int i = 0;
    while (string[i] != '\0') {//遍歷字串,直到字串結尾
        ++originalLength;//統計字串長度
        if(string[i] == '')
            ++numberOfBlank;//統計空格個數
        ++i;
    }
    /*newLength 為把空格替換成'%20'之後的長度*/
    int newLength = originalLength + numberOfBlank * 2;
    if (newLength > length)
        return;
    int indexOfOriginal = originalLength;//原字串末尾下標
    int indexOfNew = newLength;//新字串末尾下標
    //直到新字串索引下標和原字串下標在同一位置且原字串索引下標不能小於0,迴圈結束
    while (indexOfOriginal >= 0 && indexOfNew > indexOfOriginal) {
        if (string[indexOfOriginal] == '') {
            //替換,新串索引下標自減,向前移動
            string[indexOfNew--] = '0';
            string[indexOfNew--] = '2';
            string[indexOfNew--] = '%';
        }
        else {//複製原字串非空字元到新串,新串索引下標自減
            string[indexOfNew--] = string[indexOfOriginal];
        }
        //原字串索引下標向前移動
        --indexOfOriginal;
    }
}

Java版 //迴圈遍歷字串 //使用charAt()判斷是不是空格 //使用append()追加

public class Solution{
    public String replaceSpace(StringBuffer str) {
        if (str == null) {//判斷字串是否存在
            return null;
        }
        //建立替換後的新字串
        StringBuilder newStr = new StringBuilder();
        //迴圈遍歷字串
        for (int i = 0 ; i < str.length(); i++) {
            //使用charAt()判斷是不是空格
            if (str.charAt() == '') {
                newStr.append("%20");//使用append()將%20追加newstr中
            }
            else {
                //將原字串str當前字元追加到新字串newstr中
                newStr.append(str.charAt(i));
            }
        }
        //返回該物件newStr的字串
        return newStr.toString();
    }
}

面試題6:從尾到頭列印連結串列 題目描述 輸入一個連結串列,從尾到頭列印連結串列每個節點的值 C++版 通常列印是一個只讀的操作。遍歷連結串列,第一個遍歷到的節點最後一個輸出, 而最後一個遍歷到的節點第一個輸出。典型的“後進後出”,可以用棧實現這種順序。 每經過一個節點的時候,把該節點放到一個棧中。當遍歷完整個連結串列後,再從棧頂 開始逐個輸出節點的值,此時輸出的節點的順序已經反轉過來了。 STL中std::stack 類是容器介面卡,它給予程式設計師棧的功能 連結串列節點定義如下:

struct ListNode {
    int         m_nValue;
    ListNode*   m_nNext;
};
void PrintListReversingly_Iteratively(ListNode* pHead){
    //構造一個儲存泛型的棧,儲存的是連結串列資料
    std::stack<ListNode*> nodes;
    //指標pNode指向頭節點pHead
    ListNode* pNode = pHead;
    while (pNode != nullptr){//直到pNode等於空指標,迴圈結束
        nodes.push();//節點入棧
        //pNode這個指標指向它的(Next指標)下一個元素指標
        pNode = pNode->m_nNext;
    }//完成連結串列所有節點入棧
    while (!nodes.empty()){
        //pNode這個指標指向棧頂元素
        pNode = nodes.top();
        //列印pNode指標指向的元素
        printf("%d\t",pNode->m_nValue);
        //元素出棧
        nodes.pop();
    }    
}

遞迴在本質上就是一個棧結構,用遞迴實現這個函式。要實現反過來輸出連結串列, 我們每訪問到一個節點的時候,先遞迴輸出它後面的節點,再輸出該節點本身, 這樣連結串列的輸出結果就反過來了。

void PrintListReversingly_Recursively(ListNode* pHead){
    if(pHead != nullptr){//連結串列頭節點指標不為空指標
        if(pHead->m_nNext != nullptr){//頭節點Next指標不為空指標
            //遞迴呼叫方法
            PrintListReversingly_Recursively(pHead->m_nNext);
        }
        //列印pHead指標指向的元素
        printf("%d\t",pHead->m_nValue);
    }
}

基於遞迴的程式碼看起來簡潔,但是連結串列非常長的時候,就會導致函式呼叫的層級 很深,從而有可能導致函式呼叫棧溢位。顯然用棧基於迴圈實現的程式碼的魯棒性 要好一些。 Java版

import java.util.ArrayList;
//定義一個連結串列
class ListNode {
    //定義一個變數val值
    int val;
    //定義next,下一個節點的引用,預設為null
    ListNode next = null;
    //構造方法,在構造時就能給val賦值
    ListNode(int val){
        this.val = val;
    }
}
public class Solution {
    //使用遞迴的方式
    //建立arrayList存放連結串列節點的值
    ArrayList<Integer> arrayList = new ArrayList<Integer>();
    public ArrayList<Integer> printListFromTailToHead (ListNode listNode){
        if (listNode != null) {//連結串列節點不為null
            //遞迴呼叫方法
            this.printListFromTailToHead(listNode.next);
            //將連結串列節點的值新增到集合中
            arrayList.add(listNode.val);
        }
        return arrayList;
    }
}

面試題7:輸入某二叉樹的前序遍歷和中序遍歷的結果,請重建該二叉樹。 假設輸入的前序遍歷和中序遍歷的結果中都不含重複的數字。例如輸入前序遍歷序列 {1,2,4,7,3,5,6,8}和中序遍歷序列{4,7,2,1,5,3,8,6},則重建二叉樹並返回。 C++版 定義二叉樹

struct BinaryTreeNode {
    int m_nValue;
    BinaryTreeNode* m_pLeft;
    BinaryTreeNode* m_pRight;
}

在二叉樹的前序遍歷序列中,第一個數字總是樹的根節點的值。但在中序遍歷序列中, 根節點的值在序列的中間,左子樹的節點的值位於根節點值得左邊,而右子樹的節點 的值位於根節點值得右邊。因此我們需要掃描中序遍歷序列,才能找到根節點的值。 由於在中序遍歷序列中,有3個數字是左子樹節點的值,因此左子樹共有3個左子節點。同樣, 在前序遍歷序列中,根節點後面的3個數字就是3個左子樹節點的值,在後面的所有數字 都是右子樹節點的值。這樣我們就在前序遍歷和中序遍歷兩個序列中分別找到了左、右 子樹對應的子序列。我們可以用同樣的方法分別構建左子樹、右子樹。接下來的事情可以用 遞迴的方法去完成。

BinaryTreeNode* Construct(int* perorder, int* inorder, int length){
    if (preorder == nullptr || ineorder == nullptr || length <= 0)
        return nullptr;
    return ConstructCore(perorder, preorder + length -1, inorder, inorder + length -1);
}
    //startPreorder指向前序遍歷序列開頭
    //endPreorder指向前序遍歷序列末尾
    //startInorder指向中序遍歷序列開頭
    //endInorder指向中序遍歷序列末尾
BinaryTreeNode* ConstructCore (
    int* startPreorder, int* endPreorder,
    int* startInorder, int* endInorder
)
{
    //前序遍歷序列的第一個數字是根節點的值
    int rootValue = startPreorder[0];
    //建立指標指向二叉樹根節點 
    BinaryTreeNode* root = new BianryTreeNode();
    //root根指向節點值 初始化值rootValue
    root->m_nValue = rootValue
    //root根指向左子樹、右子樹 初始化nullptr
    root->m_pLeft = root->m_pRight = nullptr;
    // 只有一個元素
    if (startPreorder == endPreorder){//先序遍歷只有一個元素
        //中序遍歷只有一個元素
        if (startInorder == endInorder //startInorder和endInorder指向同一位置
            && *startPreorder == *startInorder)//且*startPreorder和*startInorder指向的值相等
            return root;//返回root節點
        else //否則,丟擲異常 無效輸入
            throw std::exception("Invaid input");
    }
    //在中序遍歷序列中找到根節點的值
    //定義、初始化指標rootInorder指向中序遍歷序列的開頭
    int* rootInorder = startInorder;
    //rootInorder指向根節點的值或rootInorder大於endInorder,迴圈結束
    while (rootInorder <= endInorder && *rootInorder != rootValue)
        ++ rootInorder;//指標rootInorder向右移動
    if (rootInorder == endInorder && *rootInorder != rootValue)
        //如果rootInorder指向中序遍歷序列的末尾且未找到根節點的值
        throw std::exception("Invaild input.");//丟擲異常,無效輸入
    //確定二叉樹的左子樹節點的長度
    int leftLength = rootInorder - startInorder;
    //定義指標指向先序遍歷序列左子樹的末尾
    int* leftPreorderEnd = startPreorder + LeftLength;
    if (leftLength > 0){//存在左子樹
        //構建左子樹
        root->m_pLeft = ConstructCore(startPreorder+1, leftPreorderEnd,
            startInorder, rootInorder - 1);
    }
    if (leftLength < endPreorder - startPreorder){
        //先序遍歷序列長度大於左子樹長度,所有存在右子樹
        //構建右子樹
        root->m_pRight = ConstructCore(leftPreorderEnd + 1, endPreorder,
            rootInorder + 1, endInorder);
    }
    
    return root;
}

在函式ConstructCore中,先根據前序遍歷序列的第一個數字建立根節點,接下來 在中序遍歷序列中找到根節點的位置,這樣就能確定左、右子樹節點的數量。 在前序遍歷和中序遍歷序列中劃分了左、右子樹節點的值後,我們就可以遞迴 地呼叫ConstructCore去分別構建它的左、右子樹。 Java版 思路:先找出根節點,然後利用遞迴方法構建二叉樹

static class TreeNode {
    int val;
    TreeNode left;
    TreeNode right;
    TreeNode(int x){val = x;}
}
public TreeNode reConstructBinaryTree(int [] pre,int [] in){
    if (pre == null || in == null) {
        return null;
    }
    if (pre.length == 0 || in.length == 0){
        return null;
    }
    if (pre.length != in.length) {
        return null;
    }
    //前序遍歷序列的第一個數字是根節點的值
    //先找出根節點
    TreeNode root = new treeNode(pre[0]);
    利用遞迴方法構建二叉樹
    for (int i = 0; i < pre.length; i++) {
        if (pre[0] == in[i]) {
            //構建左子樹
            root.left = reConstructBinaryTree(
                            Arrays.copyOfRange(pre,1,i+1),Arrays.copyOfRange(in,0,i));
            //構建右子樹                
            root.right = reConstructBinaryTree(
                            Arrays.copyOfRange(pre,i+1,pre.length),Arrays.copyOfRange(in,i+1,in.length));
        }
    }
    return root;
}

面試題8:給定一顆二叉樹和其中的一個節點,如何找出中序遍歷序列的下一個節點?樹中的節點除了有兩個分別指向 左、右子節點的指標,還有一個指向父節點的指標。 如果一個節點有右子樹,那麼它的下一個節點就是它的右子樹的左子節點。從右子節點出發一直沿著指向左子節點的指標, 我們就能找到它的下一個節點。 接下來分析一個節點沒有右子樹的情形。如果節點是它父節點的左子節點,那麼它的下一 個節點就是它的父節點。 如果一個節點既沒有右子樹,並且它還是它父節點的右子節點,我們可以沿著指向父節點的指標向上遍歷,直到找到 一個是它父節點的左子節點的節點。如果這樣的節點存在,那麼這樣的父節點就是我們要找的下一個節點。 C++版

BinaryTreeNode* GetNext(BinarytreeNode* pNode){
    if(pNode == nullptr)
        return nullptr;
    
    BinaryTreeNode* pNext = nullptr;
    if(pNode->m_pRight != nullptr){
        BinaryTreeNode* pRight = pNode->m_pRight;
        while(pRight->m_pLeft != nullptr)
            pRight = pRight->m_pLeft;
        pNext = pRight;
    }
    else if (pNode->m_pParent != nullptr){
        BinaryTreeNode* pCurrent = pNode;
        BinaryTreeNode* pParent = pNode->m_pParent;
        while(pParent != nullptr && pCurrent == pParent->m-pRight){
            pCurrent == pParent;
            pParent = pParent->m_pParent;
        }
        
        pNext = pParent;
    }
    return pNext;
}

思路:我們可發現分成兩大類:  1、有右子樹的,那麼下個結點就是右子樹最左邊的點; 2、沒有右子樹的,也可以分成兩類:  a)是父節點左孩子,那麼父節點就是下一個節點 ;  b)是父節點的右孩子,找他的父節點的父節點的父節點…直到當前結點是其父節點的左孩子位置。 如果沒有,那麼他就是尾節點,沒有下一個節點。  Java版

public class Solution {
    public TreeLinkNode GetNext(TreeLinkNode pNode){
        if (pNode == null)
            return null;
        //如果有右子樹,則找右子樹的最左節點
        if (pNode.right != null) {
            pNode = pNode.right;            
            while (pNode.left != null)
                pNode = pNode.left;
            return pNode;
        }
        //如果沒有右子樹,則找到第一個當前pNode節點是父節點左子節點的節點
        while (pNode.next != null){
            if(pNode.next.left == pNode)
                return pNode.next;
            pNode = pNode.next;
        }
        //退到了根節點仍沒有找到,則返回null
        return null;
    }
    class TreeLinkNode {
        int val;
        TreeLinkNode left = null;
        TreeLinkNode right = null;
        TreeLinkNode next = null;
        
        TreeLinkNode(int val){
            this.val = val;
        }
    }
}

面試題9:用兩個棧實現佇列 題目:用兩個棧實現一個佇列。佇列的宣告如下,請實現它的兩個函式appendTail和deleteHead, 分別完成在佇列尾部插入節點和在佇列頭部刪除節點的功能。

template<typename T> class CQueue
{
public:
    CQueue(void);
    ~CQueue(void);
    
    void appendTail(const T& node);
    T deleteHead();

private:
    stack<T> stack1;//棧1
    stack<T> stack2;//棧2
};

從佇列中插入元素。首先插入一個元素a,先把它插入stack1,再壓入兩個元素b和c,而stack2仍然是空。 從佇列中刪除一個元素。按照佇列先入先出的規則,由於a比b、c先插入佇列中,最先被刪除的元素應該是a。 總結出刪除一個元素的步驟:當stack2不為空時,在stack2中的棧頂元素是最先進入佇列的元素,可以彈出。 當stack2為空時,我們把stack1中的元素逐個彈出並壓入stack2.由於先進入佇列的元素被壓到stack1的底端, 經過彈出和壓入操作之後就處於stack2的頂端,又可以直接彈出。

template<typename T> void CQueue<T>::appendTail(const T& element){
    stack1.push(element);//棧1插入元素
}
template<typename T> T CQueue<T>::deleteHead(){
    if(stack2.size()<=0){//棧2為空
        while(stack1.size()>0){//棧1為空
            T& data = stack1.top();
            stack1.pop();//把棧1的元素逐個彈出
            stack2.push(data);//並壓入棧2
        }
    }
    if(stack2.size() == 0)
        throw new exception("queue is empty");
    T head = stack2.top();
    stack2.pop();//把棧2的元素彈出
    return head;
}

用兩個棧來實現一個佇列,完成佇列的Push和Pop操作。 佇列中的元素為int型別。 思路:一個棧壓入元素,而另一個棧作為緩衝,將棧1的元素出棧後壓入棧2中。也可以將棧1中的最後一個元素直接出棧, 而不用壓入棧2中再出棧。 Java版

public void push(int node){
    stack1.push(node);
}
public int pop() throws Exception {
    if (stack1.isEmpty() && stack2.isEmpty()){
        throw new Exception("棧為空!");
    }
    if (stack2.isEmpty()){
        while(!stack1.isEmpty()){
            stack2.push(stack1.pop());
        }
    }
    return stack2.pop();
}

相關題目:用兩個佇列實現一個棧 我們先往棧內壓入一個元素a。把a插入兩個佇列的任意一個,queue1.繼續往棧內壓入b、c兩個元素,我們把它們 都插入queue1。這個時候queue1包含3個元素a、b、c,其中a位於佇列的頭部,c位於佇列的尾部。 現在我們考慮從棧內彈出一個元素。根據棧的後入先出的原則,最後被壓入棧的c應該最先被彈出。由於c位於queue1 的尾部,而我們每次只能從佇列的頭部刪除元素,因此我們可以先從queue1中一次刪除元素a、b並插入queue2,再從 queue1中刪除元素c。這樣就相當於從棧中彈出元素c了,我們可以用同樣的方法從棧內彈出元素b。 面試題10:斐波那契數列 題目一:求斐波那契數列的第n項。 寫一個函式,輸入n,求斐波那契(Fibonacci)數列的第n項。 我們可以把已經得到的數列中間項儲存起來,在下次需要計算的時候我們先查詢一下,如果前面已經計算過就不用再重複 計算了。從下往上計算,首先根據f(0)和f(1)算出f(2),再根據f(1)和f(2)算出f(3)...以此類推就可以算出第n項了。這種 思路的時間複雜度是O(n)。

long long Fibonacci(unsigned n){
    int result[2] = {0,1};
    if(n<2)
        return result[n];
    long long fibNMinusOne = 1;
    long long fibNMinusTwo = 0;
    long long fibN = 0;
    for(usdigned int i = 2; i<=n;++i){
        fibN = fibNMinusOne+fibNMinusTwo;//從下往上計算
        fibNMinusTwo = fibNMinusOne;
        fibNMinusOne = fibN;
    }
    return fibN;
}

時間複雜度O(logn)但不夠實用的解法 把求斐波那契數列轉換成求矩陣的乘方。 題目二:青蛙跳臺階問題。 一隻青蛙一次可以跳上1級臺階,也可以跳上2級臺階。求該青蛙跳上一個n級的臺階總共有多少種跳法。有兩種跳法:一種是分 兩次跳,每次跳1級;另一種就是一次跳2級。n階臺階的不同跳法的總數f(n)=f(n-1)+f(n-2)。 Java版 思路:遞迴的效率低,使用迴圈方式。

public long fibonacci(int n){
    long result = 0;
    long preOne = 1;
    long preTwo = 0;
    if(n==0){
        return preTwo;
    }
    if(n==1){
        return preOne;
    }
    for(int i = 2; i <= n; i++){
        result = preOne + preTwo;
        preTwo = preOne;
        preOne = result;
    }
    return result;
}

本題擴充套件: 在青蛙跳臺階的問題中,如果把條件改成:一隻青蛙一次可以跳上1級臺階,也可以跳上2級......它也可以跳出n級,此時該青蛙 跳上一個n級臺階總共有多少種跳法?數學歸納法f(n)=2^n-1*1;前n-1個臺階的跳法2^n-1,面臨最後一個臺階的跳法1種。 除了最後一個臺階,每個臺階都有兩種選擇,跳或者不跳,但是最後一個臺階必須跳。