【面試必備】手撕代碼,你怕不怕

https://mp.weixin.qq.com/s/UHnGRqH88dJGnXV8yunBwQ

【面試必備】手撕代碼,你怕不【面試必備】手撕代碼,你怕不怕怕?


image.png

前言:無論是遠程的視頻面試,仍是現場的面試,都有可能會有手撕代碼的環節,這也是不少童鞋包括我(雖然還沒遇到過..)都很頭疼的東西,多是由於 IDE 自動提示功能用慣了或是其餘一些緣由,總之讓我手寫代碼就是感受很奇怪..可是我想的話,這應該側重考察的是一些細節或者是習慣方面的一些東西,因此仍是防患於未然吧,把一些可能手撕的代碼給準備準備,分享分享,但願能夠獲得各位的指正,而後能有一些討論,因爲我字太醜就不上傳本身默寫的代碼了,但仍是但願各位潦草寫一遍加深一下印象吧,以上;java


Part 1.生產者-消費者問題

這絕對是屬於重點了,無論是考察對於該重要模型的理解仍是考察代碼能力,這都是一道很好的考題,因此頗有必要的,咱們先來回顧一下什麼是生產者-消費者問題;node

問題簡單回顧

image.png

生產者消費者問題(英語:Producer-consumer problem),也稱有限緩衝問題(英語:Bounded-buffer problem),是一個多線程同步問題的經典案例。該問題描述了共享固定大小緩衝區的兩個線程——即所謂的「生產者」和「消費者」——在實際運行時會發生的問題。生產者的主要做用是生成必定量的數據放到緩衝區中,而後重複此過程。與此同時,消費者也在緩衝區消耗這些數據。該問題的關鍵就是要保證生產者不會在緩衝區滿時加入數據,消費者也不會在緩衝區中空時消耗數據。(摘自維基百科:生產者消費者問題)git

  • 注意: 生產者-消費者模式中的內存緩存區的主要功能是數據在多線程間的共享,此外,經過該緩衝區,能夠緩解生產者和消費者的性能差;github

幾種實現方式

上面說到該問題的關鍵是:如何保證生產者不會在緩衝區滿時加入數據,消費者也不會在緩衝區空時消耗數據;解決思路能夠簡單歸納爲:web

  • 生產者持續生產,直到緩衝區滿,滿時阻塞;緩衝區不滿後,繼續生產;面試

  • 消費者持續消費,直到緩衝區空,空時阻塞;緩衝區不空後,繼續消費;算法

  • 生產者和消費者均可以有多個;編程

那麼在 Java 語言中,能達到上述要求的,天然而然的就會有以下的幾種寫法,可是問題的核心都是可以讓消費者和生產者在各自知足條件須要阻塞時可以起到正確的做用:數組

  • wait()/notify()方式;緩存

  • await()/signal()方式;

  • BlockingQueue阻塞隊列方式;

  • PipedInputStream/PipedOutputStream方式;

手寫代碼,咱們着重掌握上面對應的第一種和第三種寫法就足夠了;

wait()/notify()方式實現

在手寫代碼以前,咱們須要如今 IDE 上實現一遍,理解其中的過程而且找到一些重點/細節,咱們先來代碼走一遍,而後我把我理解的重點給圈兒出來:

生產者代碼

public class Producer implements Runnable {
    private volatile boolean isRunning = true;
    private final Vector sharedQueue;                            // 內存緩衝區
    private final int SIZE;                                      // 緩衝區大小
    private static AtomicInteger count = new AtomicInteger();    // 總數,原子操做
    private static final int SLEEPTIME = 1000;

    public Producer(Vector sharedQueue, int SIZE) {
        this.sharedQueue = sharedQueue;
        this.SIZE = SIZE;
    }

    @Override
    public void run() {
        int data;
        Random r = new Random();

        System.out.println("start producer id = " + Thread.currentThread().getId());
        try {
            while (isRunning) {
                // 模擬延遲
                Thread.sleep(r.nextInt(SLEEPTIME));

                // 當隊列滿時阻塞等待
                while (sharedQueue.size() == SIZE) {
                    synchronized (sharedQueue) {
                        System.out.println("Queue is full, producer " + Thread.currentThread().getId()
                                + " is waiting, size:" + sharedQueue.size());
                        sharedQueue.wait();
                    }
                }

                // 隊列不滿時持續創造新元素
                synchronized (sharedQueue) {
                    data = count.incrementAndGet();                 // 構造任務數據
                    sharedQueue.add(data);
                    System.out.println("producer create data:" + data + ", size:" + sharedQueue.size());
                    sharedQueue.notifyAll();
                }
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
            Thread.currentThread().interrupted();
        }
    }

    public void stop() {
        isRunning = false;
    }
}

有了上面的提到的解決思路,應該很容易實現,可是這裏主要提一下一些細節和重點:

  • 創造數據:生產者-消費者解決的問題就是數據在多線程間的共享,因此咱們首要關心的問題就應該是數據,咱們這裏採用的是使用一個AtomicInteger類來爲咱們創造數據,使用它的好處是該類是一個保證原子操做的類,咱們使用其中的incrementAndGet()方法不只可以保證線程安全,還能夠達到一個計數的效果,因此是一個既簡單又實用的選擇,固然也可使用其餘的數據來代替,這裏注意的是要保證該類在內存中只存在一份,使用`static`修飾

  • 內存緩衝區:要保證在多線程環境下內存緩衝區的安全,因此咱們考慮使用簡單的Vector類來做爲咱們的內存緩衝區,而且使用final修飾保證內存緩衝區的惟一,而後的話咱們須要判斷隊列是否滿,須要手動添加一個標識緩衝區大小的變量SIZE,注意也是final修飾;

  • 模擬延遲:這裏主要模擬的是一個網絡延遲,咱們首先定義了一個SLEEPTIME的延遲範圍,注意使用的是`static final`修飾,而後使用Random()類的nextInt()方法來隨機選取一個該範圍內的值來模擬網絡環境中的延遲;

  • 中止方法:首先須要知道在Thread類中有一個棄用的stop()方法,咱們本身增長一個標誌位isRunning來完成咱們本身的stop()功能,須要注意的是使用`volatile`來修飾,保證該標誌位的可見性;

  • 錯誤處理:當捕獲到錯誤時,咱們應該使用Thread類中的interrupted()方法來終止當前的進程;

  • 消息提示:咱們主要是要在控制檯輸出該生產者的信息,包括當前隊列的狀態,大小,當前線程的生產者信息等,注意的是信息格式的統一(後面的消費者一樣的)

消費者代碼

public class Consumer implements Runnable {

    private final Vector sharedQueue;                            // 內存緩衝區
    private final int SIZE;                                      // 緩衝區大小
    private static final int SLEEPTIME = 1000;

    public Consumer(Vector sharedQueue, int SIZE) {
        this.sharedQueue = sharedQueue;
        this.SIZE = SIZE;
    }

    @Override
    public void run() {

        Random r = new Random();

        System.out.println("start consumer id = " + Thread.currentThread().getId());
        try {
            while (true) {
                // 模擬延遲
                Thread.sleep(r.nextInt(SLEEPTIME));

                // 當隊列空時阻塞等待
                while (sharedQueue.isEmpty()) {
                    synchronized (sharedQueue) {
                        System.out.println("Queue is empty, consumer " + Thread.currentThread().getId()
                                + " is waiting, size:" + sharedQueue.size());
                        sharedQueue.wait();
                    }
                }

                // 隊列不空時持續消費元素
                synchronized (sharedQueue) {
                    System.out.println("consumer consume data:" + sharedQueue.remove(0) + ", size:" + sharedQueue.size());
                    sharedQueue.notifyAll();
                }
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
            Thread.currentThread().interrupt();
        }
    }
}

跟生產者相同的,你須要注意內存緩衝區模擬延遲錯誤處理消息提示這些方面的細節問題,整體來講消費者就是持續不斷的消費,也比較容易實現;

主線程代碼

有了咱們的消費者和生產者代碼,咱們須要來驗證一下它們的正確性,照常理來講咱們直接建立一些消費者和生產者的線程讓它們執行就能夠了啊,可是爲了「加分」考慮呢,咱們仍是使用線程池吧..也不是特別複雜:

public static void main(String args[]) throws InterruptedException {
    // 1.構建內存緩衝區
    Vector sharedQueue = new Vector();
    int size = 4;

    // 2.創建線程池和線程
    ExecutorService service = Executors.newCachedThreadPool();
    Producer prodThread1 = new Producer(sharedQueue, size);
    Producer prodThread2 = new Producer(sharedQueue, size);
    Producer prodThread3 = new Producer(sharedQueue, size);
    Consumer consThread1 = new Consumer(sharedQueue, size);
    Consumer consThread2 = new Consumer(sharedQueue, size);
    Consumer consThread3 = new Consumer(sharedQueue, size);
    service.execute(prodThread1);
    service.execute(prodThread2);
    service.execute(prodThread3);
    service.execute(consThread1);
    service.execute(consThread2);
    service.execute(consThread3);

    // 3.睡一下子而後嘗試中止生產者
    Thread.sleep(10 * 1000);
    prodThread1.stop();
    prodThread2.stop();
    prodThread3.stop();

    // 4.再睡一下子關閉線程池
    Thread.sleep(3000);
    service.shutdown();
}

你們能夠自行去看看運行的結果,是沒有問題的,不會出現多生產或者多消費之類的多線程問題,運行一段時間等生產者都中止以後,咱們能夠觀察到控制檯三個消費者都在等待數據的狀況:

Queue is empty, consumer 17 is waiting, size:0
Queue is empty, consumer 15 is waiting, size:0
Queue is empty, consumer 16 is waiting, size:0

BlockingQueue阻塞隊列方式實現

該方式對比起上面一種方式實現起來要簡單一些,由於不須要手動的去通知其餘線程了,生產者直接往隊列中放數據直到隊列滿,消費者直接從隊列中獲取數據直到隊列空,BlockingQueue會自動幫咱們完成阻塞這個動做,仍是先來看看代碼

生產者代碼

public class Producer implements Runnable {
    private volatile boolean isRunning = true;
    private BlockingQueue<Integer> queue;                        // 內存緩衝區
    private static AtomicInteger count = new AtomicInteger();    // 總數,原子操做
    private static final int SLEEPTIME = 1000;

    public Producer(BlockingQueue<Integer> queue) {
        this.queue = queue;
    }

    @Override
    public void run() {
        int data;
        Random r = new Random();

        System.out.println("start producer id = " + Thread.currentThread().getId());
        try {
            while (isRunning) {
                // 模擬延遲
                Thread.sleep(r.nextInt(SLEEPTIME));

                // 往阻塞隊列中添加數據
                data = count.incrementAndGet();                 // 構造任務數據
                System.out.println("producer " + Thread.currentThread().getId() + " create data:" + data
                        + ", size:" + queue.size());
                if (!queue.offer(data, 2, TimeUnit.SECONDS)) {
                    System.err.println("failed to put data:" + data);
                }
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
            Thread.currentThread().interrupted();
        }
    }

    public void stop() {
        isRunning = false;
    }
}

跟上面一種方式沒有很大的差異,卻是代碼更加簡單通透,不過須要注意的是對阻塞隊列添加失敗的錯誤處理

消費者代碼

public class Consumer implements Runnable {

    private BlockingQueue<Integer> queue;                            // 內存緩衝區
    private static final int SLEEPTIME = 1000;

    public Consumer(BlockingQueue<Integer> queue) {
        this.queue = queue;
    }

    @Override
    public void run() {

        int data;
        Random r = new Random();

        System.out.println("start consumer id = " + Thread.currentThread().getId());
        try {
            while (true) {
                // 模擬延遲
                Thread.sleep(r.nextInt(SLEEPTIME));

                // 從阻塞隊列中獲取數據
                if (!queue.isEmpty()) {
                    data = queue.take();
                    System.out.println("consumer " + Thread.currentThread().getId() + " consume data:" + data
                            + ", size:" + queue.size());
                } else {
                    System.out.println("Queue is empty, consumer " + Thread.currentThread().getId()
                            + " is waiting, size:" + queue.size());
                }
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
            Thread.currentThread().interrupt();
        }
    }
}

主線程代碼

public static void main(String args[]) throws InterruptedException {
    // 1.構建內存緩衝區
    BlockingQueue<Integer> queue = new LinkedBlockingDeque<>();

    // 2.創建線程池和線程
    ExecutorService service = Executors.newCachedThreadPool();
    Producer prodThread1 = new Producer(queue);
    Producer prodThread2 = new Producer(queue);
    Producer prodThread3 = new Producer(queue);
    Consumer consThread1 = new Consumer(queue);
    Consumer consThread2 = new Consumer(queue);
    Consumer consThread3 = new Consumer(queue);
    service.execute(prodThread1);
    service.execute(prodThread2);
    service.execute(prodThread3);
    service.execute(consThread1);
    service.execute(consThread2);
    service.execute(consThread3);

    // 3.睡一下子而後嘗試中止生產者
    Thread.sleep(10 * 1000);
    prodThread1.stop();
    prodThread2.stop();
    prodThread3.stop();

    // 4.再睡一下子關閉線程池
    Thread.sleep(3000);
    service.shutdown();
}

由於隊列中添加和刪除的操做比較頻繁,因此這裏使用LinkedBlockingQueue來做爲阻塞隊列,因此這裏除了內存緩衝區有所不一樣之外,其餘的都差很少…固然你也能夠指定一個隊列的大小;

總結以及改進

生產者-消費者模式很好地對生產者線程和消費者線程進行解耦,優化了系統總體的結構,同時因爲緩衝區的做用,容許生產者線程和消費者線程存在執行上的性能差別,從必定程度上緩解了性能瓶頸對系統性能的影響;上面兩種寫法都是很是常規的寫法,只能說能起碼能在及格的基礎上加個那麼點兒分數,若是想要得高分能夠去搜索搜搜 Disruptor 來實現一個無鎖的生產者-消費者模型….這裏就不說起了..

改進:上面的線程輸出可能會有點兒不友好(不直觀),由於咱們這裏是直接使用的線程的 ID 來做爲輸出,咱們也能夠給線程弄一個名字來做爲輸出,以上;


Part 2.排序算法

排序算法固然也算是重點考察的對象之一了,畢竟基礎且偏算法,固然咱們有必要去了解常見的排序算法以及它們採起了怎樣的思想又是如何實現的還有複雜度的問題,可是這裏的話,主要就說起兩種考的比較常見的排序算法:冒泡快排,以及分別對它們進行的一些優化;

冒泡排序

冒泡應該是比較基礎的一種算法,咱們以從小到大排序爲例,它的基礎思想是:從第一個數開始直到數組倒數第二個數,每一輪都去比較數組中剩下的數,若是後面的數據更小則兩數交換,這樣一輪一輪的比較交換下來,最大的那個數也就「沉到」了數組的最後,最小的「冒」到了數組的最前面,這樣就完成了排序工做;

基礎算法代碼(未優化)

很簡單,直接上代碼:

/**
 * 冒泡排序
 *
 * @param nums 待排序的數組
 */

public void bubbleSort(int[] nums) {
    // 正確性判斷
    if (null == nums || nums.length <= 1) {
        return;
    }

    // 從小到大排序
    for (int i = 0; i < nums.length - 1; i++) {
        for (int j = i + 1; j < nums.length; j++) {
            if (nums[i] > nums[j]) {
                nums[i] = nums[i] + nums[j];
                nums[j] = nums[i] - nums[j];
                nums[i] = nums[i] - nums[j];
            }
        }
    }
}

這裏須要注意:加上正確性判斷;(討論:其實我看大多數地方都是沒有這個的,不知道有沒有加上的必要…求討論)

另外光寫完實現冒泡排序的算法是不算完的,還要養成良好的習慣去寫測試單元用例,並且儘量要考慮到多的點,例如這裏的負數、多個相同的數之類的特殊狀況,我就大概寫一個吧,也歡迎指正:

@Test
public void bubbleSortTester() {

    // 測試用例1:驗證負數是否知足要求
    int[] nums = {142, -2511, -70};
    bubbleSort(nums);
    // 輸出測試結果
    for (int i = 0; i < nums.length; i++) {
        System.out.print(nums[i] + ", ");
    }
    System.out.println();

    // 測試用例2:驗證多個相同的數是否知足要求
    nums = new int[]{1157731};
    bubbleSort(nums);
    // 輸出測試結果
    for (int i = 0; i < nums.length; i++) {
        System.out.print(nums[i] + ", ");
    }
}

冒泡排序優化

想象一個這樣的場景:若是該數組基本有序,或者在數組的後半段基本有序,上面的算法就會浪費許多的時間開銷,因此咱們再也不使用雙重嵌套去比較每兩個元素的值,而只是不斷比較數組每先後兩個數值,讓大的那個數不斷「冒」到數組的最後,而後縮小尾邊界的範圍,而且增長一個標誌位,表示這一趟是否發生了交換,若是沒有那麼證實該數組已經有序則完成了排序了:

/**
 * 改進的冒泡排序
 *
 * @param nums 待排序的數組
 */

public void bubbleSort2(int[] nums) {
    // 正確性判斷
    if (null == nums || nums.length <= 1) {
        return;
    }

    // 使用一個數來記錄尾邊界
    int length = nums.length;
    boolean flag = true;// 發生了交換就爲true, 沒發生就爲false,第一次判斷時必須標誌位true。
    while (flag) {
        flag = false;// 每次開始排序前,都設置flag爲未排序過
        for (int i = 1; i < length; i++) {
            if (nums[i - 1] > nums[i]) {// 前面的數字大於後面的數字就交換
                int temp;
                temp = nums[i - 1];
                nums[i - 1] = nums[i];
                nums[i] = temp;

                // 表示交換過數據;
                flag = true;
            }
        }
        length--; // 減少一次排序的尾邊界
    }
}

一樣的記得寫單元測試函數;

冒泡排序進一步優化

順着這個思路,咱們進一步想象一個場景:如今有一個包含 1000 個數的數組,僅有前面 100 個數無序,後面的 900 個數都比前面的 100 個數更大而且已經排好序,那麼上面優化的方法又會形成必定的時間浪費,因此咱們進一步增長一個變量記錄最後發生交換的元素的位置,也就是排序的尾邊界了:

/**
 * 冒泡算法最優解
 *
 * @param nums 待排序的數組
 */

public static void bubbleSort3(int[] nums) {
    int j, k;
    int flag = nums.length;// flag來記錄最後交換的位置,也就是排序的尾邊界

    while (flag > 0) {// 排序未結束標誌
        k = flag;// k 來記錄遍歷的尾邊界
        flag = 0;

        for (j = 1; j < k; j++) {
            if (nums[j - 1] > nums[j]) {// 前面的數字大於後面的數字就交換
                // 交換a[j-1]和a[j]
                int temp;
                temp = nums[j - 1];
                nums[j - 1] = nums[j];
                nums[j] = temp;

                // 表示交換過數據;
                flag = j;// 記錄最新的尾邊界.
            }
        }
    }
}

這應該是最優的冒泡排序了,同時也別忘記了最後要寫測試單元用例代碼;

快速排序

快排也是一種很經典的算法,它使用了一種分治的思想,基本思想是:經過一趟排序將待排序的數組分紅兩個部分,其中一部分記錄的是比關鍵字更小的,另外一部分是比關鍵字更大的,而後再分別對着兩部分繼續進行排序,直到整個序列有序;

基礎實現

很是經典的代碼,直接上吧:

public static void quickSort(int[] arr) {
    qsort(arr, 0, arr.length - 1);
}

private static void qsort(int[] arr, int low, int high) {
    if (low < high) {
        int pivot = partition(arr, low, high);        // 將數組分爲兩部分
        qsort(arr, low, pivot - 1);                   // 遞歸排序左子數組
        qsort(arr, pivot + 1, high);                  // 遞歸排序右子數組
    }
}

private static int partition(int[] arr, int low, int high) {
    int pivot = arr[low];               // 樞軸記錄
    while (low < high) {
        while (low < high && arr[high] >= pivot) --high;
        arr[low] = arr[high];           // 交換比樞軸小的記錄到左端
        while (low < high && arr[low] <= pivot) ++low;
        arr[high] = arr[low];           // 交換比樞軸小的記錄到右端
    }
    // 掃描完成,樞軸到位
    arr[low] = pivot;
    // 返回的是樞軸的位置
    return low;
}

固然,在手撕的時候須要注意函數上的 Java Doc 格式的註釋,這裏省略掉是爲了節省篇幅,另外別忘了測試單元用例代碼

上面的代碼也很容易理解,其實就是一個「填坑」的過程,第一個「坑」挖在每次排序的第一個位置arr[low],從序列後面往前找第一個比pivot小的數來把這個「坑」填上,這時候的「坑」就變成了當前的arr[high],而後再從序列前面日後用第一個比pivot大的數把剛纔的「坑」填上,如此往復,始終有一個「坑」須要咱們填上,直到最後一個「坑」出現,這個「坑」使用一開始的pivot填上就能夠了,而這個「坑」的位置也就是pivot該填上的正確位置,咱們再把這個位置返回,就能夠把當前序列分紅兩個部分再依次這樣操做最終就達到排序的目的了,不得不說這樣的思想挺神奇的;

算法優化

上面這個快速排序算法能夠說是最基本的快速排序,由於它並無考慮任何輸入數據。可是,咱們很容易發現這個算法的缺陷:這就是在咱們輸入數據基本有序甚至徹底有序的時候,這算法退化爲冒泡排序,再也不是O(n㏒n),而是O(n^2)了。

究其根源,在於咱們的代碼實現中,每次只從數組第一個開始取。若是咱們採用「三者取中」,即 arr[low], arr[high], arr[(low+high)/2] 三者的中值做爲樞軸記錄,則能夠大大提升快速排序在最壞狀況下的性能。可是,咱們仍然沒法將它在數組有序情形下的性能提升到O(n)。還有一些方法能夠不一樣程度地提升快速排序在最壞狀況下的時間性能。

此外,快速排序須要一個遞歸棧,一般狀況下這個棧不會很深,爲log(n)級別。可是,若是每次劃分的兩個數組長度嚴重失衡,則爲最壞狀況,棧的深度將增長到O(n)。此時,由棧空間帶來的空間複雜度不可忽略。若是加上額外變量的開銷,這裏甚至可能達到恐怖的O(n^2)空間複雜度。因此,快速排序的最差空間複雜度不是一個定值,甚至可能不在一個級別。

爲了解決這個問題,咱們能夠在每次劃分後比較兩端的長度,並先對短的序列進行排序(目的是先結束這些棧以釋放空間),能夠將最大深度降回到O(㏒n)級別。

關於優化的話,瞭解一個大概的思路就能夠了,那在這裏稍微總結一下:

  • ①三數取中做爲樞軸記錄;

  • ②當待排序序列的長度分割到必定大小以後,使用插入排序;

  • ③在一次分割結束後,能夠把與pivot相等的元素聚在一塊兒,繼續下次分割時,不用再對與pivot相等的元素分割;

  • ④優化遞歸操做;

參考文章:https://blog.51cto.com/flyingcat2013/1281614
想要了解的更多的童鞋能夠戳這裏:https://blog.csdn.net/insistGoGo/article/details/7785038


Part 3.二叉樹相關算法

二叉樹也是一個容易說起的概念和寫算法的問題,特別是它的幾種遞歸遍歷和非遞歸遍歷,都是基礎且常考的點,那在這裏就稍微整理整理吧…

二叉樹的幾種遞歸遍歷

前序、中序、後序遍歷都是很是基礎且容易的遍歷方法,重點仍是在後面的中序和後續的非遞歸遍歷上,固然還有層序遍歷,因此這裏很少說,直接給代碼;

前序遍歷遞歸實現

public void preOrderTraverse1(TreeNode root) {
    if (root != null) {
        System.out.print(root.val + "  ");
        preOrderTraverse1(root.left);
        preOrderTraverse1(root.right);
    }
}

中序遍歷遞歸實現

public void inOrderTraverse1(TreeNode root) {
    if (root != null) {
        preOrderTraverse1(root.left);
        System.out.print(root.val + "  ");
        preOrderTraverse1(root.right);
    }
}

後序遍歷遞歸實現

public void postOrderTraverse1(TreeNode root) {
    if (root != null) {
        preOrderTraverse1(root.left);
        preOrderTraverse1(root.right);
        System.out.print(root.val + "  ");
    }
}

前面三種遍歷,也就是輸出結點數據的位置不一樣而已,因此很容易,可是若是手寫,建議問清楚面試官要求,是在遍歷時直接輸出仍是須要函數返回一個List集合,而後注意寫測試用例代碼!

二叉樹的幾種非遞歸遍歷

★★層序遍歷★★

層序遍歷咱們只須要增長使用一個隊列便可,看代碼很容易理解:

public void levelTraverse(TreeNode root) {
    if (root == null) {
        return;
    }
    LinkedList<TreeNode> queue = new LinkedList<>();
    queue.offer(root);
    while (!queue.isEmpty()) {
        TreeNode node = queue.poll();
        System.out.print(node.val + "  ");
        if (node.left != null) {
            queue.offer(node.left);
        }
        if (node.right != null) {
            queue.offer(node.right);
        }
    }
}

前序遍歷非遞歸實現

public void preOrderTraverse2(TreeNode root) {
    if (root == null) {
        return;
    }
    LinkedList<TreeNode> stack = new LinkedList<>();
    TreeNode pNode = root;
    while (pNode != null || !stack.isEmpty()) {
        if (pNode != null) {
            System.out.print(pNode.val + "  ");
            stack.push(pNode);
            pNode = pNode.left;
        } else { //pNode == null && !stack.isEmpty()
            TreeNode node = stack.pop();
            pNode = node.right;
        }
    }
}

★★中序遍歷非遞歸實現★★

/**
 * 非遞歸中序遍歷二叉樹
 *
 * @param root 二叉樹根節點
 * @return 中序遍歷結果集
 */

public List<Integer> inorderTraversal(TreeNode root) {

    List<Integer> list = new ArrayList<>();
    ArrayDeque<TreeNode> stack = new ArrayDeque<>();

    while (root != null || !stack.isEmpty()) {
        while (root != null) {
            stack.addFirst(root);
            root = root.left;
        }
        root = stack.removeFirst();
        list.add(root.val);
        root = root.right;
    }
    return list;
}

★★後續遍歷非遞歸實現★★

/**
 * 二叉樹的後序遍歷
 *
 * @param root 二叉樹根節點
 * @return 後序遍歷結果集
 */

public List<Integer> postorderTraversal(TreeNode root) {
    List<Integer> list = new ArrayList<>();
    Deque<TreeNode> stack = new ArrayDeque<>();
    TreeNode pre = null;
    while (!stack.isEmpty() || root != null) {

        while (root != null) {
            stack.push(root);
            root = root.left;
        }

        root = stack.peek();
        // i :判斷若是右子樹不爲空且不爲
        if (root.right != null && root.right != pre) {
            root = root.right;
        } else {
            root = stack.pop();
            list.add(root.val);
            pre = root;
            root = null;
        }
    }
    return list;
}

若是比較難以理解的話,能夠本身嘗試着跟跟 Debug 而後看看過程;

二叉樹相關其餘算法題

另外的話還有一些比較常見的關於樹的算法,在文章的末尾,這裏就再也不贅述了:

連接:https://www.jianshu.com/p/4ef1f50d45b5


Part 4.其餘重要算法

除了上面 3 Part 比較重要的點以外,還有一些其餘的算法也是常常考到的,下面咱們來講;

1.反轉鏈表

這是一道很經典的題,不只考你對鏈表的理解,並且還有一些細節(例如正確性判斷/ 測試用例)須要你從代碼層面去展示,下面咱們給出兩段代碼,讀者能夠自行去比較,我只是提供一個思路;

思路一:使用一個 Node 不斷連接

這是最經典的算法,也是須要咱們緊緊掌握的方法,最重要的仍是理解while() 循環中的過程:

公衆號字數限制,詳情戳:https://www.jianshu.com/p/3f0cd7af370d

思路二:反轉元素值而後從新賦給 Node

這是一個很簡單的思路,比上個思路要多遍歷一遍鏈表,可是好處是簡單,思路清晰,實現起來容易,emm,像這一類問題我以爲另外一個比較重要的就是觸類旁通的能力吧,在這裏我只提供兩個思路,其實還有不少種實現方法,固然也別忘了細節的東西~

公衆號字數限制,詳情戳:https://www.jianshu.com/p/3f0cd7af370d

2.合併兩個有序鏈表

問題描述:將兩個有序鏈表合併爲一個新的有序鏈表並返回。新鏈表是經過拼接給定的兩個鏈表的全部節點組成的;

一樣的經典算法,須要掌握:

公衆號字數限制,詳情戳:https://www.jianshu.com/p/3f0cd7af370d

這道題也是 LeetCode 上的一道題,我當時的作法是下面這樣的,雖然看起來代碼量多了很多並且看起來蠢蠢的..可是通過 LeetCode 測試,甚至比上面的實現要快上那麼 2ms,特別提醒:下面的代碼只是用做一個思路的參考,着重掌握上面的代碼 :

公衆號字數限制,詳情戳:https://www.jianshu.com/p/3f0cd7af370d

3.兩個鏈表的第一個公共結點

題目描述:找出兩個鏈表的第一個公共結點;

公衆號字數限制,詳情戳:https://www.jianshu.com/p/3f0cd7af370d

須要注意的細節是:①正確性判斷;②判斷鏈表是否本身成環;③註釋;④注意要本身寫測試用例啊

另外還有一些相似的題目像是:①鏈表入環結點;②鏈表倒數第k個結點;之類的都是須要掌握的…

4.二分查找算法

二分查找也是一類比較常考的題目,其實代碼也比較容易理解,直接上吧,再再再提醒一下:注意正確性判斷還有測試用例…

普通實現

公衆號字數限制,詳情戳:https://www.jianshu.com/p/3f0cd7af370d

遞歸實現

公衆號字數限制,詳情戳:https://www.jianshu.com/p/3f0cd7af370d

5.斐波那契數列

這也是一道很經典的題,一般是要要求 N 值的範圍的,常規寫法應該很簡單,因此須要掌握的是優化以後的算法:

公衆號字數限制,詳情戳:https://www.jianshu.com/p/3f0cd7af370d

仍是注意正確性判斷而後寫測試用例…


手撕代碼總結

若是用手寫代碼的話,確實是個挺麻煩的事兒,首先須要對代碼有至關的熟悉程度,而後其次的話考察的都是一些細節的東西,例如:

  • 編碼規範:包括一些命名的規範/ 註釋的規範等等;

  • 縮進:這個我本身卻是挺在乎的..關於這個能夠去參考參考阿里出的那個規範手冊;

  • 註釋:若是命名規範作得好的話實際上是能夠達到代碼即註釋的,可是仍然有一些須要標註的地方例如函數頭之類的,最好仍是作好註釋;

  • 代碼的完整性:我以爲這個包括對於錯誤的處理/ 正確性判斷這樣一類的東西;

  • 測試用例:每一個函數都須要必定的測試來保證其正確性,因此這個仍是挺有必要的,特別是一些邊界狀況,null 值判斷之類的;

  • 想好再下筆:這一點其實也蠻重要的,無論是在紙上仍是在咱們平時的編程中,思路永遠都是更重要的;

說來講去仍是關於代碼的事,我以爲仍是理清思路最重要,因此咱們須要在一遍一遍熟悉代碼的過程當中,熟知這些代碼的思路,只有搞清楚這些代碼背後的原理了,咱們才能正確且快速的寫出咱們心中想要的代碼,而不是簡單的去背誦,這樣是沒有很大效果的,以上…


歡迎轉載,轉載請註明出處!   
簡書ID:@我沒有三顆心臟  
github:wmyskxz  
歡迎關注公衆微信號:wmyskxz_javaweb  
分享本身的Java Web學習之路以及各類Java學習資料
想要交流的朋友也能夠加qq羣:3382693


我沒有三顆心臟

讚揚二維碼鐘意做者

相關文章
相關標籤/搜索