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

前言:不管是遠(yuǎn)程的視頻面試,還是現(xiàn)場(chǎng)的面試,都有可能會(huì)有手撕代碼的環(huán)節(jié),這也是很多童鞋包括我(雖然還沒(méi)遇到過(guò)..)都很頭疼的東西,可能是因?yàn)?IDE 自動(dòng)提示功能用慣了或是其他一些原因,總之讓我手寫代碼就是感覺(jué)很奇怪..但是我想的話,這應(yīng)該側(cè)重考察的是一些細(xì)節(jié)或者是習(xí)慣方面的一些東西,所以還是防患于未然吧,把一些可能手撕的代碼給準(zhǔn)備準(zhǔn)備,分享分享,希望可以得到各位的指正,然后能有一些討論,由于我字太丑就不上傳自己默寫的代碼了,但還是希望各位潦草寫一遍加深一下印象吧,以上;


Part 1.生產(chǎn)者-消費(fèi)者問(wèn)題

這絕對(duì)是屬于重點(diǎn)了,不管是考察對(duì)于該重要模型的理解還是考察代碼能力,這都是一道很好的考題,所以很有必要的,我們先來(lái)回顧一下什么是生產(chǎn)者-消費(fèi)者問(wèn)題;

問(wèn)題簡(jiǎn)單回顧

生產(chǎn)者消費(fèi)者問(wèn)題(英語(yǔ):Producer-consumer problem),也稱有限緩沖問(wèn)題(英語(yǔ):Bounded-buffer problem),是一個(gè)多線程同步問(wèn)題的經(jīng)典案例。該問(wèn)題描述了共享固定大小緩沖區(qū)的兩個(gè)線程——即所謂的“生產(chǎn)者”和“消費(fèi)者”——在實(shí)際運(yùn)行時(shí)會(huì)發(fā)生的問(wèn)題。生產(chǎn)者的主要作用是生成一定量的數(shù)據(jù)放到緩沖區(qū)中,然后重復(fù)此過(guò)程。與此同時(shí),消費(fèi)者也在緩沖區(qū)消耗這些數(shù)據(jù)。該問(wèn)題的關(guān)鍵就是要保證生產(chǎn)者不會(huì)在緩沖區(qū)滿時(shí)加入數(shù)據(jù),消費(fèi)者也不會(huì)在緩沖區(qū)中空時(shí)消耗數(shù)據(jù)。(摘自維基百科:生產(chǎn)者消費(fèi)者問(wèn)題)

  • 注意: 生產(chǎn)者-消費(fèi)者模式中的內(nèi)存緩存區(qū)的主要功能是數(shù)據(jù)在多線程間的共享,此外,通過(guò)該緩沖區(qū),可以緩解生產(chǎn)者和消費(fèi)者的性能差;

幾種實(shí)現(xiàn)方式

上面說(shuō)到該問(wèn)題的關(guān)鍵是:如何保證生產(chǎn)者不會(huì)在緩沖區(qū)滿時(shí)加入數(shù)據(jù),消費(fèi)者也不會(huì)在緩沖區(qū)空時(shí)消耗數(shù)據(jù);解決思路可以簡(jiǎn)單概括為:

  • 生產(chǎn)者持續(xù)生產(chǎn),直到緩沖區(qū)滿,滿時(shí)阻塞;緩沖區(qū)不滿后,繼續(xù)生產(chǎn);
  • 消費(fèi)者持續(xù)消費(fèi),直到緩沖區(qū)空,空時(shí)阻塞;緩沖區(qū)不空后,繼續(xù)消費(fèi);
  • 生產(chǎn)者和消費(fèi)者都可以有多個(gè);

那么在 Java 語(yǔ)言中,能達(dá)到上述要求的,自然而然的就會(huì)有如下的幾種寫法,但是問(wèn)題的核心都是能夠讓消費(fèi)者和生產(chǎn)者在各自滿足條件需要阻塞時(shí)能夠起到正確的作用:

  • wait()/notify()方式;
  • await()/signal()方式;
  • BlockingQueue阻塞隊(duì)列方式;
  • PipedInputStream/PipedOutputStream方式;

手寫代碼,我們著重掌握上面對(duì)應(yīng)的第一種和第三種寫法就足夠了;

wait()/notify()方式實(shí)現(xiàn)

在手寫代碼之前,我們需要現(xiàn)在 IDE 上實(shí)現(xiàn)一遍,理解其中的過(guò)程并且找到一些重點(diǎn)/細(xì)節(jié),我們先來(lái)代碼走一遍,然后我把我理解的重點(diǎn)給圈兒出來(lái):

生產(chǎn)者代碼

public class Producer implements Runnable {
    private volatile boolean isRunning = true;
    private final Vector sharedQueue;                            // 內(nèi)存緩沖區(qū)
    private final int SIZE;                                      // 緩沖區(qū)大小
    private static AtomicInteger count = new AtomicInteger();    // 總數(shù),原子操作
    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));

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

                // 隊(duì)列不滿時(shí)持續(xù)創(chuàng)造新元素
                synchronized (sharedQueue) {
                    data = count.incrementAndGet();                 // 構(gòu)造任務(wù)數(shù)據(jù)
                    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;
    }
}

有了上面的提到的解決思路,應(yīng)該很容易實(shí)現(xiàn),但是這里主要提一下一些細(xì)節(jié)和重點(diǎn):

  • 創(chuàng)造數(shù)據(jù):生產(chǎn)者-消費(fèi)者解決的問(wèn)題就是數(shù)據(jù)在多線程間的共享,所以我們首要關(guān)心的問(wèn)題就應(yīng)該是數(shù)據(jù),我們這里采用的是使用一個(gè)AtomicInteger類來(lái)為我們創(chuàng)造數(shù)據(jù),使用它的好處是該類是一個(gè)保證原子操作的類,我們使用其中的incrementAndGet()方法不僅能夠保證線程安全,還可以達(dá)到一個(gè)計(jì)數(shù)的效果,所以是一個(gè)既簡(jiǎn)單又實(shí)用的選擇,當(dāng)然也可以使用其他的數(shù)據(jù)來(lái)代替,這里注意的是要保證該類在內(nèi)存中只存在一份,使用static修飾;
  • 內(nèi)存緩沖區(qū):要保證在多線程環(huán)境下內(nèi)存緩沖區(qū)的安全,所以我們考慮使用簡(jiǎn)單的Vector類來(lái)作為我們的內(nèi)存緩沖區(qū),并且使用final修飾保證內(nèi)存緩沖區(qū)的唯一,然后的話我們需要判斷隊(duì)列是否滿,需要手動(dòng)添加一個(gè)標(biāo)識(shí)緩沖區(qū)大小的變量SIZE,注意也是final修飾;
  • 模擬延遲:這里主要模擬的是一個(gè)網(wǎng)絡(luò)延遲,我們首先定義了一個(gè)SLEEPTIME的延遲范圍,注意使用的是static final修飾,然后使用Random()類的nextInt()方法來(lái)隨機(jī)選取一個(gè)該范圍內(nèi)的值來(lái)模擬網(wǎng)絡(luò)環(huán)境中的延遲;
  • 停止方法:首先需要知道在Thread類中有一個(gè)棄用的stop()方法,我們自己增加一個(gè)標(biāo)志位isRunning來(lái)完成我們自己的stop()功能,需要注意的是使用volatile來(lái)修飾,保證該標(biāo)志位的可見(jiàn)性;
  • 錯(cuò)誤處理:當(dāng)捕獲到錯(cuò)誤時(shí),我們應(yīng)該使用Thread類中的interrupted()方法來(lái)終止當(dāng)前的進(jìn)程;
  • 消息提示:我們主要是要在控制臺(tái)輸出該生產(chǎn)者的信息,包括當(dāng)前隊(duì)列的狀態(tài),大小,當(dāng)前線程的生產(chǎn)者信息等,注意的是信息格式的統(tǒng)一(后面的消費(fèi)者同樣的);

消費(fèi)者代碼

public class Consumer implements Runnable {

    private final Vector sharedQueue;                            // 內(nèi)存緩沖區(qū)
    private final int SIZE;                                      // 緩沖區(qū)大小
    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));

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

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

跟生產(chǎn)者相同的,你需要注意內(nèi)存緩沖區(qū)/ 模擬延遲/ 錯(cuò)誤處理/ 消息提示這些方面的細(xì)節(jié)問(wèn)題,總體來(lái)說(shuō)消費(fèi)者就是持續(xù)不斷的消費(fèi),也比較容易實(shí)現(xiàn);

主線程代碼

有了我們的消費(fèi)者和生產(chǎn)者代碼,我們需要來(lái)驗(yàn)證一下它們的正確性,照常理來(lái)說(shuō)我們直接創(chuàng)建一些消費(fèi)者和生產(chǎn)者的線程讓它們執(zhí)行就可以了啊,但是為了“加分”考慮呢,我們還是使用線程池吧..也不是特別復(fù)雜:

public static void main(String args[]) throws InterruptedException {
    // 1.構(gòu)建內(nèi)存緩沖區(qū)
    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.睡一會(huì)兒然后嘗試停止生產(chǎn)者
    Thread.sleep(10 * 1000);
    prodThread1.stop();
    prodThread2.stop();
    prodThread3.stop();

    // 4.再睡一會(huì)兒關(guān)閉線程池
    Thread.sleep(3000);
    service.shutdown();
}

大家可以自行去看看運(yùn)行的結(jié)果,是沒(méi)有問(wèn)題的,不會(huì)出現(xiàn)多生產(chǎn)或者多消費(fèi)之類的多線程問(wèn)題,運(yùn)行一段時(shí)間等生產(chǎn)者都停止之后,我們可以觀察到控制臺(tái)三個(gè)消費(fèi)者都在等待數(shù)據(jù)的情況:

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阻塞隊(duì)列方式實(shí)現(xiàn)

該方式對(duì)比起上面一種方式實(shí)現(xiàn)起來(lái)要簡(jiǎn)單一些,因?yàn)椴恍枰謩?dòng)的去通知其他線程了,生產(chǎn)者直接往隊(duì)列中放數(shù)據(jù)直到隊(duì)列滿,消費(fèi)者直接從隊(duì)列中獲取數(shù)據(jù)直到隊(duì)列空,BlockingQueue會(huì)自動(dòng)幫我們完成阻塞這個(gè)動(dòng)作,還是先來(lái)看看代碼

生產(chǎn)者代碼

public class Producer implements Runnable {
    private volatile boolean isRunning = true;
    private BlockingQueue<Integer> queue;                        // 內(nèi)存緩沖區(qū)
    private static AtomicInteger count = new AtomicInteger();    // 總數(shù),原子操作
    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));

                // 往阻塞隊(duì)列中添加數(shù)據(jù)
                data = count.incrementAndGet();                 // 構(gòu)造任務(wù)數(shù)據(jù)
                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;
    }
}

跟上面一種方式?jīng)]有很大的差別,倒是代碼更加簡(jiǎn)單通透,不過(guò)需要注意的是對(duì)阻塞隊(duì)列添加失敗的錯(cuò)誤處理;

消費(fèi)者代碼

public class Consumer implements Runnable {

    private BlockingQueue<Integer> queue;                            // 內(nèi)存緩沖區(qū)
    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));

                // 從阻塞隊(duì)列中獲取數(shù)據(jù)
                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.構(gòu)建內(nèi)存緩沖區(qū)
    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.睡一會(huì)兒然后嘗試停止生產(chǎn)者
    Thread.sleep(10 * 1000);
    prodThread1.stop();
    prodThread2.stop();
    prodThread3.stop();

    // 4.再睡一會(huì)兒關(guān)閉線程池
    Thread.sleep(3000);
    service.shutdown();
}

因?yàn)殛?duì)列中添加和刪除的操作比較頻繁,所以這里使用LinkedBlockingQueue來(lái)作為阻塞隊(duì)列,所以這里除了內(nèi)存緩沖區(qū)有所不同以外,其他的都差不多...當(dāng)然你也可以指定一個(gè)隊(duì)列的大?。?/p>

總結(jié)以及改進(jìn)

生產(chǎn)者-消費(fèi)者模式很好地對(duì)生產(chǎn)者線程和消費(fèi)者線程進(jìn)行解耦,優(yōu)化了系統(tǒng)整體的結(jié)構(gòu),同時(shí)由于緩沖區(qū)的作用,允許生產(chǎn)者線程和消費(fèi)者線程存在執(zhí)行上的性能差異,從一定程度上緩解了性能瓶頸對(duì)系統(tǒng)性能的影響;上面兩種寫法都是非常常規(guī)的寫法,只能說(shuō)能起碼能在及格的基礎(chǔ)上加個(gè)那么點(diǎn)兒分?jǐn)?shù),如果想要得高分可以去搜索搜搜 Disruptor 來(lái)實(shí)現(xiàn)一個(gè)無(wú)鎖的生產(chǎn)者-消費(fèi)者模型....這里就不提及了..

改進(jìn):上面的線程輸出可能會(huì)有點(diǎn)兒不友好(不直觀),因?yàn)槲覀冞@里是直接使用的線程的 ID 來(lái)作為輸出,我們也可以給線程弄一個(gè)名字來(lái)作為輸出,以上;


Part 2.排序算法

排序算法當(dāng)然也算是重點(diǎn)考察的對(duì)象之一了,畢竟基礎(chǔ)且偏算法,當(dāng)然我們有必要去了解常見(jiàn)的排序算法以及它們采取了怎樣的思想又是如何實(shí)現(xiàn)的還有復(fù)雜度的問(wèn)題,但是這里的話,主要就提及兩種考的比較常見(jiàn)的排序算法:冒泡快排,以及分別對(duì)它們進(jìn)行的一些優(yōu)化;

冒泡排序

冒泡應(yīng)該是比較基礎(chǔ)的一種算法,我們以從小到大排序?yàn)槔?,它的基礎(chǔ)思想是:從第一個(gè)數(shù)開(kāi)始直到數(shù)組倒數(shù)第二個(gè)數(shù),每一輪都去比較數(shù)組中剩下的數(shù),如果后面的數(shù)據(jù)更小則兩數(shù)交換,這樣一輪一輪的比較交換下來(lái),最大的那個(gè)數(shù)也就“沉到”了數(shù)組的最后,最小的“冒”到了數(shù)組的最前面,這樣就完成了排序工作;

基礎(chǔ)算法代碼(未優(yōu)化)

很簡(jiǎn)單,直接上代碼:

/**
 * 冒泡排序
 *
 * @param nums 待排序的數(shù)組
 */
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];
            }
        }
    }
}

這里需要注意:加上正確性判斷;(討論:其實(shí)我看大多數(shù)地方都是沒(méi)有這個(gè)的,不知道有沒(méi)有加上的必要...求討論)

另外光寫完實(shí)現(xiàn)冒泡排序的算法是不算完的,還要養(yǎng)成良好的習(xí)慣去寫測(cè)試單元用例,而且盡可能要考慮到多的點(diǎn),例如這里的負(fù)數(shù)、多個(gè)相同的數(shù)之類的特殊情況,我就大概寫一個(gè)吧,也歡迎指正:

@Test
public void bubbleSortTester() {

    // 測(cè)試用例1:驗(yàn)證負(fù)數(shù)是否滿足要求
    int[] nums = {1, 4, 2, -2, 5, 11, -7, 0};
    bubbleSort(nums);
    // 輸出測(cè)試結(jié)果
    for (int i = 0; i < nums.length; i++) {
        System.out.print(nums[i] + ", ");
    }
    System.out.println();

    // 測(cè)試用例2:驗(yàn)證多個(gè)相同的數(shù)是否滿足要求
    nums = new int[]{1, 1, 5, 7, 7, 3, 1};
    bubbleSort(nums);
    // 輸出測(cè)試結(jié)果
    for (int i = 0; i < nums.length; i++) {
        System.out.print(nums[i] + ", ");
    }
}

冒泡排序優(yōu)化

想象一個(gè)這樣的場(chǎng)景:如果該數(shù)組基本有序,或者在數(shù)組的后半段基本有序,上面的算法就會(huì)浪費(fèi)許多的時(shí)間開(kāi)銷,所以我們不再使用雙重嵌套去比較每?jī)蓚€(gè)元素的值,而只是不斷比較數(shù)組每前后兩個(gè)數(shù)值,讓大的那個(gè)數(shù)不斷“冒”到數(shù)組的最后,然后縮小尾邊界的范圍,并且增加一個(gè)標(biāo)志位,表示這一趟是否發(fā)生了交換,如果沒(méi)有那么證明該數(shù)組已經(jīng)有序則完成了排序了:

/**
 * 改進(jìn)的冒泡排序
 *
 * @param nums 待排序的數(shù)組
 */
public void bubbleSort2(int[] nums) {
    // 正確性判斷
    if (null == nums || nums.length <= 1) {
        return;
    }
    
    // 使用一個(gè)數(shù)來(lái)記錄尾邊界
    int length = nums.length;
    boolean flag = true;// 發(fā)生了交換就為true, 沒(méi)發(fā)生就為false,第一次判斷時(shí)必須標(biāo)志位true。
    while (flag) {
        flag = false;// 每次開(kāi)始排序前,都設(shè)置flag為未排序過(guò)
        for (int i = 1; i < length; i++) {
            if (nums[i - 1] > nums[i]) {// 前面的數(shù)字大于后面的數(shù)字就交換
                int temp;
                temp = nums[i - 1];
                nums[i - 1] = nums[i];
                nums[i] = temp;

                // 表示交換過(guò)數(shù)據(jù);
                flag = true;
            }
        }
        length--; // 減小一次排序的尾邊界
    }
}

同樣的記得寫單元測(cè)試函數(shù);

冒泡排序進(jìn)一步優(yōu)化

順著這個(gè)思路,我們進(jìn)一步想象一個(gè)場(chǎng)景:現(xiàn)在有一個(gè)包含 1000 個(gè)數(shù)的數(shù)組,僅有前面 100 個(gè)數(shù)無(wú)序,后面的 900 個(gè)數(shù)都比前面的 100 個(gè)數(shù)更大并且已經(jīng)排好序,那么上面優(yōu)化的方法又會(huì)造成一定的時(shí)間浪費(fèi),所以我們進(jìn)一步增加一個(gè)變量記錄最后發(fā)生交換的元素的位置,也就是排序的尾邊界了:

/**
 * 冒泡算法最優(yōu)解
 *
 * @param nums 待排序的數(shù)組
 */
public static void bubbleSort3(int[] nums) {
    int j, k;
    int flag = nums.length;// flag來(lái)記錄最后交換的位置,也就是排序的尾邊界

    while (flag > 0) {// 排序未結(jié)束標(biāo)志
        k = flag;// k 來(lái)記錄遍歷的尾邊界
        flag = 0;

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

                // 表示交換過(guò)數(shù)據(jù);
                flag = j;// 記錄最新的尾邊界.
            }
        }
    }
}

這應(yīng)該是最優(yōu)的冒泡排序了,同時(shí)也別忘記了最后要寫測(cè)試單元用例代碼;

快速排序

快排也是一種很經(jīng)典的算法,它使用了一種分治的思想,基本思想是:通過(guò)一趟排序?qū)⒋判虻臄?shù)組分成兩個(gè)部分,其中一部分記錄的是比關(guān)鍵字更小的,另一部分是比關(guān)鍵字更大的,然后再分別對(duì)著兩部分繼續(xù)進(jìn)行排序,直到整個(gè)序列有序;

基礎(chǔ)實(shí)現(xiàn)

非常經(jīng)典的代碼,直接上吧:

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);        // 將數(shù)組分為兩部分
        qsort(arr, low, pivot - 1);                   // 遞歸排序左子數(shù)組
        qsort(arr, pivot + 1, high);                  // 遞歸排序右子數(shù)組
    }
}

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;
}

當(dāng)然,在手撕的時(shí)候需要注意函數(shù)上的 Java Doc 格式的注釋,這里省略掉是為了節(jié)省篇幅,另外別忘了測(cè)試單元用例代碼;

上面的代碼也很容易理解,其實(shí)就是一個(gè)“填坑”的過(guò)程,第一個(gè)“坑”挖在每次排序的第一個(gè)位置arr[low],從序列后面往前找第一個(gè)比pivot小的數(shù)來(lái)把這個(gè)“坑”填上,這時(shí)候的“坑”就變成了當(dāng)前的arr[high],然后再?gòu)男蛄星懊嫱笥玫谝粋€(gè)比pivot大的數(shù)把剛才的“坑”填上,如此往復(fù),始終有一個(gè)“坑”需要我們填上,直到最后一個(gè)“坑”出現(xiàn),這個(gè)“坑”使用一開(kāi)始的pivot填上就可以了,而這個(gè)“坑”的位置也就是pivot該填上的正確位置,我們?cè)侔堰@個(gè)位置返回,就可以把當(dāng)前序列分成兩個(gè)部分再依次這樣操作最終就達(dá)到排序的目的了,不得不說(shuō)這樣的思想挺神奇的;

算法優(yōu)化

上面這個(gè)快速排序算法可以說(shuō)是最基本的快速排序,因?yàn)樗](méi)有考慮任何輸入數(shù)據(jù)。但是,我們很容易發(fā)現(xiàn)這個(gè)算法的缺陷:這就是在我們輸入數(shù)據(jù)基本有序甚至完全有序的時(shí)候,這算法退化為冒泡排序,不再是O(n㏒n),而是O(n^2)了。

究其根源,在于我們的代碼實(shí)現(xiàn)中,每次只從數(shù)組第一個(gè)開(kāi)始取。如果我們采用“三者取中”,即 arr[low], arr[high], arr[(low+high)/2] 三者的中值作為樞軸記錄,則可以大大提高快速排序在最壞情況下的性能。但是,我們?nèi)匀粺o(wú)法將它在數(shù)組有序情形下的性能提高到O(n)。還有一些方法可以不同程度地提高快速排序在最壞情況下的時(shí)間性能。

此外,快速排序需要一個(gè)遞歸棧,通常情況下這個(gè)棧不會(huì)很深,為log(n)級(jí)別。但是,如果每次劃分的兩個(gè)數(shù)組長(zhǎng)度嚴(yán)重失衡,則為最壞情況,棧的深度將增加到O(n)。此時(shí),由棧空間帶來(lái)的空間復(fù)雜度不可忽略。如果加上額外變量的開(kāi)銷,這里甚至可能達(dá)到恐怖的O(n^2)空間復(fù)雜度。所以,快速排序的最差空間復(fù)雜度不是一個(gè)定值,甚至可能不在一個(gè)級(jí)別。

為了解決這個(gè)問(wèn)題,我們可以在每次劃分后比較兩端的長(zhǎng)度,并先對(duì)短的序列進(jìn)行排序(目的是先結(jié)束這些棧以釋放空間),可以將最大深度降回到O(㏒n)級(jí)別。

關(guān)于優(yōu)化的話,了解一個(gè)大概的思路就可以了,那在這里稍微總結(jié)一下:

  • ①三數(shù)取中作為樞軸記錄;
  • ②當(dāng)待排序序列的長(zhǎng)度分割到一定大小之后,使用插入排序;
  • ③在一次分割結(jié)束后,可以把與pivot相等的元素聚在一起,繼續(xù)下次分割時(shí),不用再對(duì)與pivot相等的元素分割;
  • ④優(yōu)化遞歸操作;

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


Part 3.二叉樹(shù)相關(guān)算法

二叉樹(shù)也是一個(gè)容易提及的概念和寫算法的問(wèn)題,特別是它的幾種遞歸遍歷和非遞歸遍歷,都是基礎(chǔ)且常考的點(diǎn),那在這里就稍微整理整理吧...

二叉樹(shù)的幾種遞歸遍歷

前序、中序、后序遍歷都是非?;A(chǔ)且容易的遍歷方法,重點(diǎn)還是在后面的中序和后續(xù)的非遞歸遍歷上,當(dāng)然還有層序遍歷,所以這里不多說(shuō),直接給代碼;

前序遍歷遞歸實(shí)現(xiàn)

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

中序遍歷遞歸實(shí)現(xiàn)

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

后序遍歷遞歸實(shí)現(xiàn)

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

前面三種遍歷,也就是輸出結(jié)點(diǎn)數(shù)據(jù)的位置不同而已,所以很容易,但是如果手寫,建議問(wèn)清楚面試官要求,是在遍歷時(shí)直接輸出還是需要函數(shù)返回一個(gè)List集合,然后注意寫測(cè)試用例代碼!

二叉樹(shù)的幾種非遞歸遍歷

★★層序遍歷★★

層序遍歷我們只需要增加使用一個(gè)隊(duì)列即可,看代碼很容易理解:

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);
        }
    }
}

前序遍歷非遞歸實(shí)現(xiàn)

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;
        }
    }
}

★★中序遍歷非遞歸實(shí)現(xiàn)★★

/**
 * 非遞歸中序遍歷二叉樹(shù)
 *
 * @param root 二叉樹(shù)根節(jié)點(diǎn)
 * @return 中序遍歷結(jié)果集
 */
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;
}

★★后續(xù)遍歷非遞歸實(shí)現(xiàn)★★

/**
 * 二叉樹(shù)的后序遍歷
 *
 * @param root 二叉樹(shù)根節(jié)點(diǎn)
 * @return 后序遍歷結(jié)果集
 */
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 :判斷如果右子樹(shù)不為空且不為
        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 然后看看過(guò)程;

二叉樹(shù)相關(guān)其他算法題

另外的話還有一些比較常見(jiàn)的關(guān)于樹(shù)的算法,在文章的末尾,這里就不再贅述了:

鏈接:http://m.itdecent.cn/p/4ef1f50d45b5


Part 4.其他重要算法

除了上面 3 Part 比較重要的點(diǎn)之外,還有一些其他的算法也是經(jīng)??嫉降?,下面我們來(lái)說(shuō);

1.反轉(zhuǎn)鏈表

這是一道很經(jīng)典的題,不僅考你對(duì)鏈表的理解,而且還有一些細(xì)節(jié)(例如正確性判斷/ 測(cè)試用例)需要你從代碼層面去展現(xiàn),下面我們給出兩段代碼,讀者可以自行去比較,我只是提供一個(gè)思路;

思路一:使用一個(gè) Node 不斷鏈接

這是最經(jīng)典的算法,也是需要我們牢牢掌握的方法,最重要的還是理解 while() 循環(huán)中的過(guò)程:


public ListNode reverseList(ListNode head) {

    // 正確性判斷
    if (null == head || null == head.next) {
        return head;
    }

    ListNode pre = null;
    while (null != head) {
        ListNode temp = head;
        head = head.next;
        temp.next = pre;
        pre = temp;
    }

    return pre;
}

思路二:反轉(zhuǎn)元素值然后重新賦給 Node

這是一個(gè)很簡(jiǎn)單的思路,比上個(gè)思路要多遍歷一遍鏈表,但是好處是簡(jiǎn)單,思路清晰,實(shí)現(xiàn)起來(lái)容易,emm,像這一類問(wèn)題我覺(jué)得另一個(gè)比較重要的就是舉一反三的能力吧,在這里我只提供兩個(gè)思路,其實(shí)還有很多種實(shí)現(xiàn)方法,當(dāng)然也別忘了細(xì)節(jié)的東西~

public ListNode reverseList(ListNode head) {
    // 1.正確性判斷
    if (null == head || null == head.next) {
        return head;
    }

    // 2.遍歷鏈表head并將結(jié)果保存在List集合中
    List<ListNode> list = new LinkedList();
    ListNode tempNode = head;
    while (null != tempNode) {
        list.insert(tempNode.val);
        tempNode = tempNode.next;
    }   // end while:遍歷完了鏈表并將結(jié)果保存在了List集合中

    // 3.反轉(zhuǎn)集合,并將值依次復(fù)制給鏈表
    Collections.reverse(list);
    tempNode = head;
    while (null != tempNode) {
        tempNode.val = list.remove(0);
        tempNode = tempNode.next;
    }

    return head;
}

2.合并兩個(gè)有序鏈表

問(wèn)題描述:將兩個(gè)有序鏈表合并為一個(gè)新的有序鏈表并返回。新鏈表是通過(guò)拼接給定的兩個(gè)鏈表的所有節(jié)點(diǎn)組成的;

同樣的經(jīng)典算法,需要掌握:

public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
    if (l1 == null) {
        return l2;
    }
    if (l2 == null) {
        return l1;
    }
    ListNode head = null;
    if (l1.val < l2.val) {
        head = l1;
        head.next = mergeTwoLists(l1.next, l2);
    } else {
        head = l2;
        head.next = mergeTwoLists(l1, l2.next);
    }
    return head;
}

這道題也是 LeetCode 上的一道題,我當(dāng)時(shí)的做法是下面這樣的,雖然看起來(lái)代碼量多了不少而且看起來(lái)蠢蠢的..但是經(jīng)過(guò) LeetCode 測(cè)試,甚至比上面的實(shí)現(xiàn)要快上那么 2ms,特別提醒:下面的代碼只是用作一個(gè)思路的參考,著重掌握上面的代碼

public ListNode mergeTwoLists(ListNode l1, ListNode l2) {

    // 定義一個(gè)虛擬頭結(jié)點(diǎn)方便遍歷
    ListNode dummyHead = new ListNode(-1);
    dummyHead.next = l1;
    ListNode pre = dummyHead;

    // 遍歷l1鏈表
    int len1 = 0;
    while (null != pre.next) {
        len1++;
        pre = pre.next;
    }

    int[] nums1 = new int[len1];

    // 保存l1鏈表的數(shù)據(jù)
    pre = dummyHead;
    for (int i = 0; i < len1; i++) {
        nums1[i] = pre.next.val;
        pre = pre.next;
    }

    // 遍歷l2鏈表
    int len2 = 0;
    dummyHead.next = l2;
    pre = dummyHead;
    while (null != pre.next) {
        len2++;
        pre = pre.next;
    }

    int[] nums2 = new int[len2];

    // 保存l2鏈表的數(shù)據(jù)
    pre = dummyHead;
    for (int i = 0; i < len2; i++) {
        nums2[i] = pre.next.val;
        pre = pre.next;
    }

    int[] nums = new int[len1 + len2];
    // 將兩個(gè)鏈表的數(shù)據(jù)整合并排序
    System.arraycopy(nums1, 0, nums, 0, len1);
    System.arraycopy(nums2, 0, nums, len1, len2);
    Arrays.sort(nums);

    // 拼接一個(gè)鏈表
    ListNode dummy = new ListNode(-1);
    pre = dummy;
    for (int i = 0; i < nums.length; i++) {
        ListNode node = new ListNode(nums[i]);
        pre.next = node;
        pre = pre.next;
    }

    return dummy.next;
}

3.兩個(gè)鏈表的第一個(gè)公共結(jié)點(diǎn)

題目描述:找出兩個(gè)鏈表的第一個(gè)公共結(jié)點(diǎn);

/**
 * 求兩個(gè)鏈表中第一個(gè)公共結(jié)點(diǎn)
 *
 * @param pHead1 鏈表1頭結(jié)點(diǎn)
 * @param pHead2 鏈表2頭結(jié)點(diǎn)
 * @return 鏈表第一個(gè)公共結(jié)點(diǎn)
 */
public ListNode FindFirstCommonNode(ListNode pHead1, ListNode pHead2) {
    // 1.正確性判斷
    if (null == pHead1 || null == pHead2) {
        return null;
    }

    // 2.遍歷鏈表1把所有結(jié)點(diǎn)保存在列表中中
    Vector<ListNode> nodeList1 = new Vector<>();
    while (null != pHead1) {
        nodeList1.add(pHead1);
        pHead1 = pHead1.next;
        // 判斷是否成環(huán),成環(huán)則退出循環(huán)
        if (nodeList1.contains(pHead1)) {
            break;
        }
    }   // end while:鏈表1中的所有結(jié)點(diǎn)都存入了nodeList1中

    // 3.遍歷鏈表2比較列表中的數(shù)據(jù)
    Vector<ListNode> nodeList2 = new Vector<>();
    while (null != pHead2) {
        // 先判斷nodeList1中是否存在相同結(jié)點(diǎn),存在則返回
        if (nodeList1.contains(pHead2)) {
            return pHead2;
        }
        // 如果不存在則繼續(xù)遍歷,并判斷是否成環(huán)
        nodeList2.add(pHead2);
        pHead2 = pHead2.next;
        if (nodeList2.contains(pHead2)) {
            break;
        }
    }   // end while:遍歷完了鏈表2并將所有結(jié)點(diǎn)保存在了nodeList2中

    // 如果遍歷完鏈表2則返回null
    return null;
}

需要注意的細(xì)節(jié)是:①正確性判斷;②判斷鏈表是否自己成環(huán);③注釋;④注意要自己寫測(cè)試用例啊...

另外還有一些類似的題目像是:①鏈表入環(huán)結(jié)點(diǎn);②鏈表倒數(shù)第k個(gè)結(jié)點(diǎn);之類的都是需要掌握的...

4.二分查找算法

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

普通實(shí)現(xiàn)

/**
 * 二分查找普通實(shí)現(xiàn)。
 *
 * @param srcArray 有序數(shù)組
 * @param key      查找元素
 * @return 不存在返回-1
 */
public static int binSearch(int srcArray[], int key) {
    int mid;
    int start = 0;
    int end = srcArray.length - 1;
    while (start <= end) {
        mid = (end - start) / 2 + start;
        if (key < srcArray[mid]) {
            end = mid - 1;
        } else if (key > srcArray[mid]) {
            start = mid + 1;
        } else {
            return mid;
        }
    }
    return -1;
}

遞歸實(shí)現(xiàn)

/**
 * 二分查找遞歸實(shí)現(xiàn)。
 *
 * @param srcArray 有序數(shù)組
 * @param start    數(shù)組低地址下標(biāo)
 * @param end      數(shù)組高地址下標(biāo)
 * @param key      查找元素
 * @return 查找元素不存在返回-1
 */
public static int binSearch(int srcArray[], int start, int end, int key) {
    int mid = (end - start) / 2 + start;
    if (srcArray[mid] == key) {
        return mid;
    }
    if (start >= end) {
        return -1;
    } else if (key > srcArray[mid]) {
        return binSearch(srcArray, mid + 1, end, key);
    } else if (key < srcArray[mid]) {
        return binSearch(srcArray, start, mid - 1, key);
    }
    return -1;
}

5.斐波那契數(shù)列

這也是一道很經(jīng)典的題,通常是要要求 N 值的范圍的,常規(guī)寫法應(yīng)該很簡(jiǎn)單,所以需要掌握的是優(yōu)化之后的算法:

public int Fibonacci(int n) {
    // 正確性判斷
    if (0 == n || 1 == n) {
        return n;
    }

    int nums1 = 0, nums2 = 1;
    int res = 0;
    for (int i = 2; i <= n; i++) {
        res = nums1 + nums2;
        nums1 = nums2;
        nums2 = res;
    }

    return res;
}

還是注意正確性判斷然后寫測(cè)試用例...


手撕代碼總結(jié)

如果用手寫代碼的話,確實(shí)是個(gè)挺麻煩的事兒,首先需要對(duì)代碼有相當(dāng)?shù)氖煜こ潭?,然后其次的話考察的都是一些?xì)節(jié)的東西,例如:

  • 編碼規(guī)范:包括一些命名的規(guī)范/ 注釋的規(guī)范等等;
  • 縮進(jìn):這個(gè)我自己倒是挺在意的..關(guān)于這個(gè)可以去參考參考阿里出的那個(gè)規(guī)范手冊(cè);
  • 注釋:如果命名規(guī)范做得好的話其實(shí)是可以達(dá)到代碼即注釋的,但是仍然有一些需要標(biāo)注的地方例如函數(shù)頭之類的,最好還是做好注釋;
  • 代碼的完整性:我覺(jué)得這個(gè)包括對(duì)于錯(cuò)誤的處理/ 正確性判斷這樣一類的東西;
  • 測(cè)試用例:每個(gè)函數(shù)都需要一定的測(cè)試來(lái)保證其正確性,所以這個(gè)還是挺有必要的,特別是一些邊界情況,null 值判斷之類的;
  • 想好再下筆:這一點(diǎn)其實(shí)也蠻重要的,不管是在紙上還是在我們平時(shí)的編程中,思路永遠(yuǎn)都是更重要的;

說(shuō)來(lái)說(shuō)去還是關(guān)于代碼的事,我覺(jué)得還是理清思路最重要,所以我們需要在一遍一遍熟悉代碼的過(guò)程中,熟知這些代碼的思路,只有搞清楚這些代碼背后的原理了,我們才能正確且快速的寫出我們心中想要的代碼,而不是簡(jiǎn)單的去背誦,這樣是沒(méi)有很大效果的,以上...


歡迎轉(zhuǎn)載,轉(zhuǎn)載請(qǐng)注明出處!
簡(jiǎn)書ID:@我沒(méi)有三顆心臟
github:wmyskxz
歡迎關(guān)注公眾微信號(hào):wmyskxz_javaweb
分享自己的Java Web學(xué)習(xí)之路以及各種Java學(xué)習(xí)資料
想要交流的朋友也可以加qq群:3382693

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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

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