【面試】網易遊戲面試題目整理及答案(4)

網易遊戲面試題目整理及答案(4)

算法部分

  1. 什麼是二叉堆?
    答:二叉堆本質上是一種完全二叉樹,它分爲兩個類型:最大堆最小堆
    最大堆:最大堆任何一個父節點的值,都大於等於它左右孩子節點的值
    最小堆:最小堆任何一個父節點的值,都小於等於它左右孩子節點的值
    二叉堆的根節點叫做堆頂最大堆和最小堆的特點,決定了在最大堆的堆頂是整個堆中的最大元素;最小堆的堆頂是整個堆中最小元素
    問題1:如何構建一個堆呢?
    答:這需要依靠二叉堆的自我調整。堆的自我調整過程如下:
    對於二叉堆,有幾種操作:①插入節點;②刪除節點;③構建二叉堆。這幾種操作都是基於堆的自我調整。以最小堆爲例:
    ①插入節點
    二叉樹的節點插入,插入位置是完全二叉樹的最後一個位置。比如插入一個新的節點,值是0.
    插入節點0
    這個時候,讓節點0和它的父節點5做比較,如果0小於5,則讓新節點“上浮”,和父節點交換位置。
    和父節點交換位置
    繼續用節點0和父節點3做比較,如果0小於3,則讓新節點繼續“上浮”.
    和父節點比較
    繼續比較,最終讓新節點0上浮到了堆頂位置。
    最終位置
    ②刪除節點
    二叉堆的節點刪除過程和插入過程正好相反,所刪除的是處於堆頂的節點。比如我們刪除最小堆的堆頂節點1。
    刪除堆頂元素
    這個時候,爲了維持完全二叉樹的結構,把堆的最後一個節點10補到原本堆頂的位置。
    二叉堆補位
    接下來,讓移動到堆頂的節點10和它的左右孩子進行比較,如果左右孩子中最小的一個節點(顯然是節點2)比節點10小,那麼讓節點10“下沉”。
    節點下沉
    繼續讓節點10和它的左右孩子做比較,左右孩子中最小的是節點7,由於10大於7,讓節點10繼續“下沉”.
    節點下沉
    這樣一來,二叉堆重新得到了調整。
    ③構建二叉堆
    構建二叉堆,也就是
    把一個無序的完全二叉樹調整爲二叉堆
    本質上就是讓所有非葉子節點依次下沉。例如,如下是一個無序的完全二叉樹:
    無序的完全二叉樹
    首先從最後一個非葉子節點開始,也就是從節點10開始。如果節點10大於它的左右孩子中最小的一個,則節點10下沉。
    節點下沉
    接下來,輪到節點3,如果節點3大於它左右孩子中最小的一個,則節點3下沉。
    節點3下沉
    接下來輪到節點1,如果節點1大於它左右孩子中最小的一個,則節點1下沉。事實上節點1小於它的左右孩子,所以不用改變。
    接下來輪到節點7,如果節點7大於它左右孩子中最小的一個,則節點7下沉。
    節點7下沉
    節點7繼續比較,繼續下沉。
    節點7下沉
    這樣一來,一顆無序的完全二叉樹就構建成了一個最小堆。

堆的代碼實現:
需要明確的是:二叉堆雖然是一顆完全二叉樹,但它的存儲方式並不是鏈式存儲,而是順序存儲。即二叉堆的所有節點都存儲在數組當中
二叉堆的順序存儲
問題是:在數組中,在沒有左右指針的情況下,如何定位到一個父節點的左孩子和右孩子呢?
答:像圖中那樣,可以依靠數組下標來計算。
假設父節點的下標是parent,那麼它的左孩子下標就是2*parent+1;它的右孩子下標就是2*parent+2。
比如上面的例子中,節點6包含9和10兩個孩子,節點6在數組中的下標是3,節點9在數組中的下標是7,節點10在數組中的下標是8。
7=32+17=3*2+1
8=32+28=3*2+2
剛好符合規律。代碼如下:

import java.util.Arrays;

public class HeapOperator {
    /**
     * 上浮調整
     *
     * @param array 待調整的堆
     */
    public static void upAdjust(int[] array) {
        int childIndex = array.length - 1; // 插入的節點一定是左孩子,即lChild=2*parent+1
        int parentIndex = (childIndex - 1) / 2;
        // temp保存插入的葉子節點值,用於最後的賦值
        int temp = array[childIndex];
        while (childIndex > 0 && temp < array[parentIndex]) {
            //無需真正交換,單向賦值即可
            array[childIndex] = array[parentIndex];
            childIndex = parentIndex;
            parentIndex = (parentIndex - 1) / 2; // ??
        }
        array[childIndex] = temp;
    }

    /**
     * 下沉調整
     *
     * @param array       待調整的堆
     * @param parentIndex 要下沉的父節點
     * @param length      堆的有效大小
     */
    public static void downAdjust(int[] array, int parentIndex, int length) {
        // temp 保存父節點的值,用於最後的賦值
        int temp = array[parentIndex];
        int childIndex = 2 * parentIndex + 1;
        while (childIndex < length) {
            //如果有右孩子,且右孩子小於左孩子的值,則定位到右孩子
            if (childIndex + 1 < length && array[childIndex + 1] < array[childIndex]) {
                childIndex++;
            }
            //如果父節點小於任何一個孩子的值,直接跳出
            if (temp <= array[childIndex])
                break;
            //無需真正交換,單向賦值即可
            array[parentIndex] = array[childIndex];
            parentIndex = childIndex;
            childIndex = 2 * parentIndex + 1;
        }
        array[parentIndex] = temp;
    }

    /**
     * 構建堆
     *
     * @param array 待調整的堆
     */
    public static void buildHeap(int[] array) {
        //從最後一個非葉子節點開始,依次下沉調整
        for (int i = array.length / 2; i >= 0; i--) {
            downAdjust(array, i, array.length - 1);
        }
    }

    public static void main(String[] args) {
        int[] array = new int[]{1, 3, 2, 6, 5, 7, 8, 9, 10, 0};
        upAdjust(array);
        System.out.println(Arrays.toString(array));

        array = new int[]{7, 1, 3, 10, 5, 2, 6, 8, 9};
        buildHeap(array);
        System.out.println(Arrays.toString(array));
    }
}

代碼中有一個優化的點,就是父節點和孩子節點做連續交換時,並不一定要真的交換,只需要先把交換一方的值存入temp變量,做單向覆蓋,循環結束後,再把temp的值存入交換後的最終位置。
二叉堆的用處:是實現堆排序以及優先級隊列的基礎。

  1. 什麼是堆排序?
    答:首先回顧二叉堆和最大堆的特性:①二叉堆本質上是一種完全二叉樹;②最大堆的堆頂是整個堆中的最大元素。當我們刪除一個最大堆的堆頂(並不是完全刪除,而是替換到最後面),經過自我調節,第二大的元素就會被交換上來,成爲最大堆的新堆頂。
    堆的變化過程
    正如上圖所示,當我們刪除值爲10的堆頂節點,經過調整,值爲9的新節點就會頂替上來,當我們刪除值爲9的堆頂節點,經過調節,值爲8的新節點就會頂替上來…
    由於二叉堆的這個特性,我們每一次刪除舊堆頂,調整後的新堆頂都是大小僅次於舊堆頂的節點。那麼我們只要反覆刪除堆頂,反覆調節二叉堆,所得到的集合就成爲了一個有序集合,過程如下:
    1)刪除節點9,節點8成爲新堆頂:
    刪除節點9
    2)刪除節點8,節點7成爲新堆頂:
    刪除節點8
    3)刪除節點7,節點6成爲新堆頂
    刪除節點7
    4)刪除節點6,節點5成爲新堆頂:
    刪除節點6
    5)刪除節點5,節點4成爲新堆頂:
    刪除節點5
    6)刪除節點4,節點3成爲新堆頂:
    刪除節點4
    7)刪除節點3,節點2成爲新堆頂:
    刪除節點3
    到此爲止,我們原本的最大堆已經變成了一個從小到達的有序集合。之前說過二叉堆實際存儲再數組當中,數組中的元素排列如下:
    有序數組
    由此,可以歸納出堆排序算法的步驟:
    ①把無序數組構建成二叉堆
    ②循環刪除堆頂元素,移到集合尾部,調節堆產生新的堆頂。
    代碼如下:
import java.util.Arrays;

public class HeapSort {
    public static void main(String[] args) {
        int[] arr = new int[]{1, 3, 2, 6, 5, 7, 8, 9, 10, 0};
        heapSort(arr);
        System.out.println(Arrays.toString(arr));
    }

    /**
     * 堆排序
     *
     * @param array 待調整的堆
     */
    private static void heapSort(int[] array) {
        // 1. 把無序數組構建成二叉堆
        for (int i = (array.length - 2) / 2; i >= 0; i--) {
            downAdjust(array, i, array.length);
        }
        System.out.println(Arrays.toString(array));
        // 2. 循環刪除堆頂元素,移到集合尾部,調整堆產生新的堆頂。
        for (int i = array.length - 1; i > 0; i--) {
            // 最後一個元素和第一個元素交換
            int temp = array[i];
            array[i] = array[0];
            array[0] = temp;
            //下沉調整最大堆
            downAdjust(array, 0, i);
        }
    }

    /**
     * 下沉調整
     *
     * @param array       待調整的堆
     * @param parentIndex 要下沉的父節點
     * @param length      堆的有效大小
     */
    private static void downAdjust(int[] array, int parentIndex, int length) {
        // temp保存父節點值,用於最後的賦值
        int temp = array[parentIndex];
        int childIndex = 2 * parentIndex + 1;
        while (childIndex < length) {
            //如果有右孩子,且右孩子大於左孩子的值,則定位到右孩子
            if (childIndex + 1 < length && array[childIndex + 1] > array[childIndex]) {
                childIndex++;
            }
            //如果父節點大於任何一個孩子的值,直接跳出
            if (temp >= array[childIndex])
                break;
            //無需真正交換,單向賦值即可
            array[parentIndex] = array[childIndex];
            parentIndex = childIndex;
            childIndex = 2 * childIndex + 1;
        }
        array[parentIndex] = temp;
    }
}
  1. 求二叉樹深度
    答:對於一顆二叉樹,從根節點到葉節點依次經過的節點(含根、葉節點)行程樹的一條路徑,最長路徑的長度爲樹的深度。
    思路:遞歸思路,根的高度等於(左子樹的高度和右子樹的高度中高度較高的那一個高度)+1。
    代碼如下:
class TreeNode {
    int val = 0;
    TreeNode left = null;
    TreeNode right = null;

    public TreeNode(int val) {
        this.val = val;
    }
}

public class TreeDepth {
    public int treeDepth(TreeNode root) {
        if (root == null) {
            return 0;
        }
        if (root.left == null && root.right == null) {
            return 1;
        }
        //左右子樹高度中取較高的那一個高度+1
        return treeDepth(root.left) > treeDepth(root.right) ? treeDepth(root.left) + 1 : treeDepth(root.right) + 1;
    }
}
  1. Top k問題
    答:對於該問題有四種解題思路:
    1)最直接的方法:排序
    既然是要前 K 大的數,那麼最直接的當然就是排序了,通過如快排等效率較高的排序算法,可以在平均 O(nlogn)的時間複雜度找到結果。這種方式在數據量不大的時候簡單可行,但固然不是最優的方法。
    2)O(n) 時間複雜度的方法
    剛剛提到了快排,熟悉算法題的小夥伴應該知道,快排的 partition 劃分思想可以用於計算某個位置的數值等問題,例如用來計算中位數;顯然,也適用於計算 TopK 問題
    快速排序找中位數
    每次經過劃分,如果中間值等於K,那麼其左邊的數就是Top K的數據;當然,如果不等於,只要遞歸處理左邊或者右邊的數即可。
    該方法的時間複雜度是 O(n) ,簡單分析就是第一次劃分時遍歷數組需要花費 n,而往後每一次都折半(當然不是準確地折半),粗略地計算就是 n + n/2 + n/4 +… < 2n,因此顯然時間複雜度是 O(n)。
    對比第一個方法顯然快了不少,隨着數據量的增大,兩個方法的時間差距會越來越大
    缺點:雖然時間複雜度是 O(n) ,但是缺點也很明顯,最主要的就是內存問題在海量數據的情況下,我們很有可能沒辦法一次性將數據全部加載入內存,這個時候這個方法就無法完成使命了;還有一點就是這種思路需要我們修改輸入的數組,這也是值得考慮的一點。
    3)利用分佈式思想處理海量數據
    面對海量數據,我們就可以放分佈式的方向去思考了。我們可以將數據分散在多臺機器中,然後每臺機器並行計算各自的 TopK 數據,最後彙總,再計算得到最終的 TopK 數據。這種數據分片的分佈式思想在面試中非常值得一提,在實際項目中也十分常見。
    4)利用最經典的方法,一臺機器也能處理海量數據
    其實提到 Top K 問題,最經典的解法還是利用堆維護一個大小爲 K 的小頂堆,依次將數據放入堆中,當堆的大小滿了的時候,只需要將堆頂元素與下一個數比較:如果大於堆頂元素,則將當前的堆頂元素拋棄,並將該元素插入堆中。遍歷完全部數據,Top K 的元素也自然都在堆裏面了。
    將數據插入堆
    將數據插入堆
    將95插入堆
    95大於20,進行替換
    數據下沉
    95下沉,維持小頂堆
    對於海量數據,我們不需要一次性將全部數據取出來,可以一次只取一部分,因爲我們
    只需要將數據一個個拿來與堆頂比較

    另外還有一個優勢就是對於動態數組,我們可以一直都維護一個 K 大小的小頂堆,當有數據被添加到集合中時,我們就直接拿它與堆頂的元素對比。這樣,無論任何時候需要查詢當前的前 K 大數據,我們都可以裏立刻返回給他
    整個操作中,遍歷數組需要 O(n) 的時間複雜度,一次堆化操作需要 O(logK),加起來就是 O(nlogK) 的複雜度,換個角度來看,如果 K 遠小於 n 的話, O(nlogK) 其實就接近於 O(n) 了,甚至會更快,因此也是十分高效的。
    最後,對於 Java,我們可以直接使用優先隊列 PriorityQueue 來實現一個小頂堆,這裏給個代碼:
import java.util.ArrayList;
import java.util.List;
import java.util.PriorityQueue;
import java.util.Queue;

public class TopK {
    public static List<Integer> solutionByHeap(int[] input, int k) {
        List<Integer> list = new ArrayList<>();
        if (k > input.length || k == 0) {
            return list;
        }
        Queue<Integer> queue = new PriorityQueue<>();
        for (int num : input) {
            if (queue.size() < k) {
                queue.add(num);
            } else if (queue.peek() < num) {
                queue.poll();
                queue.add(num);
            }
        }
        while (k-- > 0) {
            list.add(queue.poll());
        }
        return list;
    }
}

當然,如果是求前 K 個最小的數,只需要改爲大頂堆即可

  1. 請完成一個函數,輸入一顆二叉樹,請函數輸出它的鏡像,如下所示
    二叉樹的鏡像
    答:求出一個二叉樹的鏡像,首先要知道什麼是二叉樹的鏡像,通過上圖可以得出,鏡像就是二叉樹的每層節點的左右子樹進行相互交換。說白了就是除根節點外,所有的結點中的左子節點的鏡像是右子節點,右子節點的鏡像變成了左子節點。
    因爲每個具有非空節點的節點的左右子節點都要進行交換,所以我們可以用遞歸來解決。
    首先,使用遞歸要找到遞歸的終止條件,當我們遇到葉子節點的時候,就不用進行遞歸交換了。所以遞歸條件就是當前遞歸的節點是否爲空。
if(root==null){
	return;
}

然後聲明一個臨時變量用來存儲兩個節點交換的值,然後進行左右子樹交換。

//進行節點交換
Let tempNode = root.left;
root.left = root.right;
root.right = tempNode;

交換之後,直接遞歸剩下的節點進行交換就OK。然後返回遞歸後的樹的根節點。

//遞歸遍歷剩餘的子節點
insert(root.left)
insert(root.right)

//返回根節點
return root;

完整代碼如下:

public class TreeMirror {
    public static void mirror(TreeNode root) {
        //如果根節點爲空,無需處理
        if (root == null) {
            return;
        }
        //左子節點和右子節點都爲空,無需處理
        if (root.left == null && root.right == null) {
            return;
        }
        //左子節點不爲空,則對該子節點做鏡像處理
        if (root.left != null) {
            mirror(root.left);
        }
        //右子節點不爲空,則對該子節點做鏡像處理
        if (root.right != null) {
            mirror(root.right);
        }
        //交換當前節點的左右子節點位置
        TreeNode temp = root.left;
        root.left = root.right;
        root.right = temp;
    }
}
  1. 有一個單向鏈表,如何判斷鏈表有沒有環?
    有環鏈表
    答:方法描述如下:
    方法1:首先從頭節點開始,依次遍歷單鏈表的每一個節點。每遍歷到一個新節點,就從頭節點重新遍歷新節點之前的所有節點,用新節點ID與此節點之前的所有節點ID依次比較。如果發現新節點之前的所有節點當中存在相同節點ID,則說明該節點被遍歷過兩次,鏈表有環;如果之前的所有節點當中不存在相同的節點,就繼續遍歷下一個新節點,繼續重複剛纔的操作。
    例如這樣的鏈表:A->B->C->D->B->C->D, 當遍歷到節點D的時候,我們需要比較的是之前的節點A、B、C,不存在相同節點。這時候要遍歷的下一個新節點是B,B之前的節點A、B、C、D中恰好也存在B,因此B出現了兩次,判斷出鏈表有環。
    假設從鏈表頭節點到入環點的距離是D,鏈表的環長是S。那麼
    算法的時間複雜度
    是0+1+2+3+…+(D+S-1) = (D+S-1)(D+S)/2 , 可以簡單地理解成 **O(NN)**。而此算法沒有創建額外存儲空間,空間複雜度可以簡單地理解成爲O(1)

方法2:首先創建一個以節點ID爲鍵的HashSet集合,用來存儲曾經遍歷過的節點。然後同樣是從頭節點開始,依次遍歷單鏈表的每一個節點。每遍歷到一個新節點,就用新節點和HashSet集合當中存儲的節點作比較,如果發現HashSet當中存在相同節點ID,則說明鏈表有環;如果HashSet當中不存在相同的節點ID,就把這個新節點的ID存入HashSet,之後進入下一個節點,繼續重複剛纔的操作。
這個方法在流程上和方法一類似,本質的區別是
使用了HashSet作爲額外的緩存

假設從鏈表頭節點到入環點的距離是D,鏈表的環長是S。而每一次HashSet查找元素的時間複雜度是O(1), 所以總體的時間複雜度是1*(D+S)=D+S,可以簡單理解爲O(N)。而算法的空間複雜度還是D+S-1,可以簡單地理解成O(N)

方法3:首先創建兩個指針1和2(在java裏就是兩個對象的引用),同時指向這個鏈表的頭節點,然後開始一個大循環,在循環體中,讓指針1每次向下移動一個節點,讓指針2每次向下移動兩個節點,然後比較兩個指針指向的節點是否相同,如果相同,則判斷出鏈表有環,如果不同,則繼續下一次循環。
例如鏈表A->B->C->D->B->C->D,兩個指針最初都指向節點A,進入第一輪循環,指針1移動到了節點B,指針2移動到了C。第二輪循環,指針1移動到了節點C,指針2移動到了節點B。第三輪循環,指針1移動到了節點D,指針2移動到了節點D,此時兩指針指向同一節點,判斷出鏈表有環。
此方法也可以用一個更生動的例子來形容:在一個環形跑道上,兩個運動員在同一地點起跑,一個運動員速度快,一個運動員速度慢。當兩人跑了一段時間,速度快的運動員必然會從速度慢的運動員身後再次追上並超過,原因很簡單,因爲跑道是環形的。
假設從鏈表頭節點到入環點的距離是D,鏈表的環長是S。那麼循環會進行S次(爲什麼是S次,有心的同學可以自己揣摩下),可以簡單理解爲O(N)。除了兩個指針以外,沒有使用任何額外存儲空間,所以空間複雜度是O(1)
|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
補充問題:
問題1:判斷兩個單向鏈表是否相交,如果相交,求出交點。
單鏈表相交
答:注意相交的定義基於節點的引用,而不是基於節點的值。換句話說,如果一個鏈表的第k個節點與另一個鏈表的第j個節點是同一節點(引用完全相同),則這兩個鏈表相交。
由於鏈表本身的性質,如果有一個結點相交,那麼相交結點之後的所有結點都是這兩個鏈表共用的,也就是說兩個鏈表的長度主要相差在相交結點之前的結點長度,於是我們有以下思路:
1)如果鏈表沒有長度,則我們先遍歷這兩個鏈表拿到兩個鏈表長度,假設分別爲L1,L2(L1>=L2),定義p1,p2指針分別指向各自鏈表head結點,然後p1先往前走L1-L2步。這一步保證了p1,p2指向的指針與相交節點(如果有的話)一樣近。
2)然後 p1,p2 不斷往後遍歷,每次走一步,邊遍歷邊判斷相應結點是否相等,如果相等即爲這兩個鏈表的相交結點。

問題2:在一個有環鏈表中,如何找出鏈表的入環點?
入環點
答:繼續用快慢指針來解決,如下所示:
入環點檢測
H: 鏈表頭; A: 入環點 ;B: 快慢指針相交點
先做如下約定:
LHA: 鏈表頭H到入環點A的距離;
LAB: 鏈表節點A順時針到節點B的距離;
LBA: 鏈表節點B順時針到節點A的距離;
根據移動距離,可知:
2慢指針移動距離 = 快指針移動距離,即2(LHA + LAB) = LHA + LAB + LBA + LAB;
最後推導出:LHA = LBA
所以,只要從相交點和頭節點同時遍歷到的相同節點就能找到入環點.
代碼如下:


import java.util.HashSet;
import java.util.Set;

public class CycleListNode {
    static class ListNode {
        int val;
        ListNode next;

        public ListNode(int data) {
            this.val = data;
        }
    }

    /**
     * 快慢指針
     */
    public static boolean hasCycleByPoint(ListNode head) {
        boolean hasCycle = true;
        ListNode fastIndex = head;
        ListNode slowIndex = head;
        do {
            if (fastIndex == null) {
                hasCycle = false;
                break;
            }
            if (fastIndex.next == null) {
                hasCycle = false;
                break;
            }
            fastIndex = fastIndex.next.next;
            slowIndex = slowIndex.next;
        } while (fastIndex != slowIndex);
        return hasCycle;
    }

    /**
     * hash
     */
    public static boolean hasCycleByHash(ListNode head) {
        boolean hasCycle = false;
        Set set = new HashSet();
        while (head != null) {
            if (set.contains(head)) {
                hasCycle = true;
                break;
            }
            set.add(head);
            head = head.next;
        }
        return hasCycle;
    }

    /**
     * 單鏈表第一個相交點
     * @param head1
     * @param head2
     * @return
     */
    public static ListNode getFirstCommonNode(ListNode head1, ListNode head2) {
        int length1 = 0; //鏈表1的長度
        int length2 = 0; //鏈表2的長度

        ListNode p1 = head1;
        ListNode p2 = head2;

        while (p1.next != null) {
            length1++;
            p1 = p1.next;
        }
        while (p2.next != null) {
            length2++;
            p2 = p2.next;
        }
        p1 = head1;
        p2 = head2;

        //p1或p2前進|length1-length2|步
        if (length1 >= length2) {
            int diffLen = length1 - length2;
            while (diffLen > 0) {
                p1 = p1.next;
                diffLen--;
            }
        } else {
            int diffLen = length2 - length1;
            while (diffLen > 0) {
                p2 = p2.next;
                diffLen--;
            }
        }
        //p1,p2分別往後遍歷,邊遍歷邊比較,如果相等,即爲第一個相交節點
        while (p1 != null && p2.next != null) {
            p1 = p1.next;
            p2 = p2.next;
            if (p1.val == p2.val) {
                //p1,p2都爲相交節點,返回p1或p2
                return p1;
            }
        }
        //如果沒有相交節點,返回空指針
        return null;
    }

    /**
     * 環形相交點
     * F:頭結點到入環結點距離
     * B:入環結點到快慢指針相交結點距離
     * C:快慢指針相交結點到入環結點距離
     * 2*慢指針移動距離=快指針移動距離
     * 2(F+B)=F+B+B+C
     * F = C
     *
     * @param head
     * @return
     */
    public static ListNode getEntranceNode(ListNode head) {
        ListNode fastIndex = head;
        ListNode slowIndex = head;
        do {
            if (fastIndex == null) {
                break;
            }
            if (fastIndex.next == null) {
                fastIndex = fastIndex.next;
                break;
            }
            fastIndex = fastIndex.next.next;
            slowIndex = slowIndex.next;
        } while (fastIndex != slowIndex);
        if (fastIndex == null) {
            return null;
        }
        ListNode headIndex = head;
        while (fastIndex != headIndex) {
            fastIndex = fastIndex.next;
            headIndex = headIndex.next;
        }
        return fastIndex;
    }

    public static void main(String[] args) {
        test1();
        test2();
        test3();
    }

    private static void test1() {
        ListNode n1 = new ListNode(1);
        ListNode n2 = new ListNode(2);
        ListNode n3 = new ListNode(3);
        ListNode n4 = new ListNode(4);
        ListNode n5 = new ListNode(5);
        n1.next = n2;
        n2.next = n3;
        n3.next = n4;
        n4.next = n5;
        n5.next = n2;
        System.out.println(hasCycleByPoint(n1));
        assert getEntranceNode(n1) != null;
        System.out.println(getEntranceNode(n1).val);
    }

    private static void test2() {
        ListNode n1 = new ListNode(1);
        ListNode n2 = new ListNode(2);
        ListNode n3 = new ListNode(3);
        ListNode n4 = new ListNode(4);
        ListNode n5 = new ListNode(5);
        n1.next = n2;
        n2.next = n3;
        n3.next = n4;
        n4.next = n5;
        System.out.println(hasCycleByPoint(n1));
        System.out.println(getEntranceNode(n1));
    }

    private static void test3() {
        ListNode n1 = new ListNode(1);
        ListNode n2 = new ListNode(2);
        ListNode n3 = new ListNode(3);
        ListNode n4 = new ListNode(4);
        ListNode n5 = new ListNode(5);
        n1.next = n2;
        n2.next = n3;
        n3.next = n4;
        n4.next = n5;
        System.out.println(getFirstCommonNode(n1, n2).val);
    }
}

|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|

  1. 遞歸翻轉鏈表
    答:什麼是鏈表的翻轉:給定鏈表 head–>4—>3–>2–>1,將其翻轉成 head–>1–>2–>3–>4 。
    首先我們要查看翻轉鏈表是否符合遞歸規律問題可以分解成具有相同解決思路的子問題,子子問題…,直到最終的子問題再也無法分解。
    要翻轉 head—>4—>3–>2–>1 鏈表,不考慮 head 結點,分析 4—>3–>2–>1,仔細觀察我們發現只要先把 3–>2–>1 翻轉成 3<----2<----1,之後再把 3 指向 4 即可(如下圖示)
    反轉鏈表
    圖:翻轉鏈表主要三步驟
    只要按以上步驟定義好這個翻轉函數的功能即可, 這樣由於子問題與最初的問題具有相同的解決思路,拆分後的子問題持續調用這個翻轉函數即可達到目的。
    注意看上面的步驟1,問題的規模是不是縮小了(如下圖),從翻轉整個鏈表變成了只翻轉部分鏈表!問題與子問題都是從某個結點開始翻轉,具有相同的解決思路,另外當縮小到只翻轉一個結點時,顯然是終止條件,符合遞歸的條件!之後的翻轉 3–>2–>1, 2–>1 持續調用這個定義好的遞歸函數即可!
    遞歸反轉
    注意翻轉之後 head 的後繼節點變了,需要重新設置!別忘了這一步
    1、定義遞歸函數,明確函數的功能。根據以上分析,這個遞歸函數的功能顯然是翻轉某個節點開始的鏈表,然後返回新的頭節點。
    2、尋找遞推公式。上文中已經詳細畫出了翻轉鏈表的步驟,簡單總結一下遞推步驟如下:
  • 針對節點node(值爲4),先翻轉node之後的結點,invert(node->next),翻轉之後4->3->2->1變成了4->3<-2<-1
  • 再把node節點的下個節點(3)指向node,node的後繼節點設置爲空(避免形成環),此時變成了4<-3<-2<-1
  • 返回新的頭結點,因爲此時新的頭節點從原來的4變成了1,需要重新設置一下head
    3、將遞推公式代入第一步定義好的函數中(invertLinkedList)
    4、計算時間/空間複雜度。由於遞歸調用了n次invertLinkedList函數,所以時間複雜度雖然是O(n)。空間複雜度呢?沒有用到額外的空間,但是由於遞歸調用了n次invertLinkedList函數,壓了n次棧,所以空間複雜度也是O(n)。
    遞歸一定要從函數的功能去理解,從函數的功能看,定義的遞歸函數清晰易懂,定義好了之後,由於問題與被拆分的子問題具有相同的解決思路,所以子問題只要持續調用定義好的功能函數即可,切勿層層展開子問題!
    遞歸翻轉鏈表
    補充:非遞歸翻轉鏈表(迭代解法)
    遞歸比較容易造成棧溢出,所以如果有其他時間/空間複雜度相近或更好的算法,應該優先選擇非遞歸的解法,對於該問題,主要思路如下:
    非遞歸翻轉鏈表示意圖
    步驟1,定義兩個結點,pre,cur,其中cur是pre的後繼結點,如果是首次定義,需要把pre指向cur的指針去掉,否則由於之後鏈表翻轉,cur會指向pre,就進行了一個環(如下),這一點需要注意:
    步驟1
    步驟2,知道了cur和pre,翻轉就容易了,把cur指向pre即可,之後把cur設置爲pre,cur的後繼結點設置爲cur一直往前重複此步驟即可。注意:同遞歸翻轉一樣,迭代翻轉完了之後 head 的後繼結點從 4 變成了 1,記得重新設置一下。
/**
 * 迭代翻轉
 */
public void iterationInvertLinkedList() {
    // 步驟 1
    Node pre = head.next;
    Node cur = pre.next;
    pre.next = null;

    while (cur != null) {
        /**
         * 務必注意:在 cur 指向 pre 之前一定要先保留 cur 的後繼結點,不然 cur 指向 pre 後就再也找不到後繼結點了
         * 也就無法對 cur 後繼之後的結點進行翻轉了
         */
        Node next = cur.next;
        cur.next = pre;
        pre = cur;
        cur = next;
    }
    // 此時 pre 爲頭結點的後繼結點
    head.next = pre;
}

用迭代的思路來做由於循環了 n 次,顯然時間複雜度爲 O(n),另外由於沒有額外的空間使用,也未像遞歸那樣調用遞歸函數不斷壓棧,所以空間複雜度是 O(1),對比遞歸,顯然應該使用迭代的方式來處理!
完整代碼如下:

public class LinkedList {
    int length = 0; //鏈表長度,非必須
    Node head = new Node(0); //哨兵節點

    public void addNode(int val) {
        Node tmp = head;
        while (tmp.next != null) {
            tmp = tmp.next;
        }
        tmp.next = new Node(val);
        length++;
    }

    public void printList() {
        head = this.head.next;
        do {
            System.out.println(head.data);
            head = this.head.next;
        } while (this.head != null);
    }

    public void headInsert(int val) {
        // 1. 構造新節點
        Node newNode = new Node(val);
        //2. 新節點指向頭節點之後的節點
        newNode.next = head.next;
        // 3. 頭節點指向新節點
        head.next = newNode;
        length++;
    }

    /**
     * 刪除指定的結點
     *
     * @param deletedNode
     */
    public void removeSelectedNode(Node deletedNode) {
        // 如果此結點是尾結點,我們還是要從頭節點遍歷到尾結點的前繼結點,再將尾結點刪除
        if (deletedNode.next == null) {
            Node tmp = head;
            while (tmp.next != deletedNode) {
                tmp = tmp.next;
            }
            //找到尾結點的前繼結點
            tmp.next = null;
        } else {
            Node nextNode = deletedNode.next;
            //將刪除結點的後繼節點的值賦給被刪除結點
            deletedNode.data = nextNode.data;
            //將nextNode結點刪除
            deletedNode.next = nextNode.next;
            nextNode.next = null;
        }
    }

    /**
     * 遞歸翻轉結點node開始的鏈表
     *
     * @param node
     * @return
     */
    public Node invertLinkedList(Node node) {
        if (node.next == null) {
            return node;
        }
        // 步驟1:先翻轉node之後的鏈表
        Node newHead = invertLinkedList(node.next);
        // 步驟2:再把原node節點後繼節點的後繼節點指向node(4),node的後繼節點設置爲空(防止形成環)
        node.next.next = node;
        node.next = null;
        // 步驟3:返回翻轉後的頭節點
        return newHead;
    }

    /**
     * 迭代式翻轉鏈表
     */
    public void iterationInvertLinkedList() {
        // 步驟1
        Node pre = head.next;
        Node cur = pre.next;
        pre.next = null;

        while (cur != null) {
            //在cur指向pre之前一定要先保留cur和後繼結點
            Node next = cur.next;
            cur.next = pre;
            pre = cur;
            cur = next;
        }
        // 此時pre爲頭結點的後繼結點
        head.next = pre;
    }

    public static void main(String[] args) {
        LinkedList linkedList = new LinkedList();
        int[] arr = {4, 3, 2, 1};
        //頭插發構造鏈表
        for (int i = 0; i < arr.length; i++) {
            linkedList.addNode(arr[i]);
        }
        Node newHead = linkedList.invertLinkedList(linkedList.head.next);
        //翻轉後別忘了設置頭結點的後繼結點
        linkedList.head.next = newHead;
        //打印鏈表
        linkedList.printList();
    }
}

  1. 如何用兩個棧表示一個隊列
    答:棧的特點是先入後出,出入元素都是在同一端(棧頂):
    入棧:
    入棧
    出棧:
    出棧
    隊列的特點是先入先出,出入元素是在不同的兩端(隊頭和隊尾):
    入隊:
    入隊
    出隊:
    出隊
    既然我們擁有兩個棧,那麼我們可以讓其中一個棧作爲隊列的入口,負責插入新元素;另一個棧作爲隊列的出口,負責移除老元素
    用兩個棧模擬隊列
    問題:兩個棧是各自獨立的,怎麼能把它們有效關聯起來呢?
    隊列的
    主要操作無非有兩個:入隊和出隊
    在模擬入隊操作時,每一個新元素都被壓入到棧A當中。
    讓元素1“入隊”:
    讓元素1入棧
    棧的狀態
    讓元素2 “入隊”:
    元素2入棧
    棧的狀態
    讓元素3“入隊”:
    元素3入棧
    元素3的狀態
    這時候,我們希望最先“入隊”的元素1“出隊”,需要怎麼做呢?
    讓棧A中的所有元素按順序出棧,再按照出棧順序壓入棧B。這樣一來,元素從棧A彈出並壓入棧B的順序是3,2,1,和當初進入棧A的順序1,2,3是相反的:
    將棧A中的數據入棧B
    此時讓元素1”出隊“,也就是讓元素1從棧B彈出:
    元素1出棧
    讓元素2出棧
    元素2出棧
    這個時候,如果想做入隊操作了,只需要重新把新元素壓入棧A,此時的出隊操作仍然從棧B彈出元素。
    如果棧B空了,只要棧A還有元素,就把棧A元素彈出壓入棧B。
    代碼如下:
package DataStructure;

import java.util.Stack;

public class StackforQueue {
    private Stack<Integer> stackA = new Stack<>();
    private Stack<Integer> stackB = new Stack<>();

    /**
     * 入隊操作
     *
     * @param element
     */
    private void enQueue(int element) {
        stackA.push(element);
    }

    /**
     * 出隊操作
     *
     * @return
     */
    private Integer deQueue() {
        if (stackB.isEmpty()) {
            if (stackA.isEmpty()) {
                return null;
            }
            transfer();
        }
        return stackB.pop();
    }

    /**
     * 將棧A的元素轉移到棧B
     */
    private void transfer() {
        while (!stackA.isEmpty()) {
            stackB.push(stackA.pop());
        }
    }

    public static void main(String[] args) {
        StackforQueue stackforQueue = new StackforQueue();
        stackforQueue.enQueue(1);
        stackforQueue.enQueue(2);
        stackforQueue.enQueue(3);
        System.out.println(stackforQueue.deQueue());
        System.out.println(stackforQueue.deQueue());
        stackforQueue.enQueue(4);
        System.out.println(stackforQueue.deQueue());
        System.out.println(stackforQueue.deQueue());
    }
}

入隊的時間複雜度顯然是O(1)。至於出隊操作,如果涉及到棧A和棧B的元素遷移,時間複雜度爲O(n),如果不用遷移,時間複雜度爲O(1)。

  1. 不用中間元素交換兩個元素的方法,(答:使用異或),又問:不使用異或有什麼缺點。。
    答:這裏介紹兩個方法,其基本原理就是數的中和。也就是說,通過某種運算(二元運算)將a和b兩個數變成一個數,並保存其中一個變量中。然後再通過同樣的運算符將a或者b中和掉。這樣實際上是利用了a和b本身作爲了中間變量。
    第一種方法:使用異或,這是利用了異或運算的性質:一個數和兩個相同的數異或,值不變。
    第二種方法:通過“+”運算符將a和b的運算結果賦給了a(這時a是中間變量)。然後再計算b,這時a的值已經是(a+b)了,因此,a再減b就是原來的a。 而這時b已經是原來的a了,因此,再用運算後的a(實際上是a+b)減運算後的b(實際上是原來的a),就是原來的b了,最後將這個b賦值給a。實際上,我們還可以使用“*”、“/”等符號來實現同樣的效果
    全部代碼如下:
package DataStructure;

public class Exchange {
    public static void main(String[] args) {
        int numa = -3;
        int numb = -9;
        int[] num = exchange3(numa, numb);
        System.out.println(num[0] + " " + num[1]);
    }

    /**
     * 利用異或交換
     * @param numa
     * @param numb
     * @return
     */
    private static int[] exchange1(int numa, int numb) {
        int[] num = new int[2];
        numa = numa ^ numb;
        numb = numa ^ numb;
        numa = numa ^ numb;
        num[0] = numa;
        num[1] = numb;
        return num;
    }

    /**
     * 利用加減法交換
     * @param numa
     * @param numb
     * @return
     */
    public static int[] exchange2(int numa, int numb) {
        int[] num = new int[2];
        numa = numa + numb;
        numb = numa - numb;
        numa = numa - numb;
        num[0] = numa;
        num[1] = numb;
        return num;
    }

    /**
     * 利用乘除法交換
     * 注意,除數不能爲0
     * @param numa
     * @param numb
     * @return
     */
    public static int[] exchange3(int numa, int numb) {
        int[] num = new int[2];
        numa = numa * numb;
        numb = numa / numb;
        numa = numa / numb;
        num[0] = numa;
        num[1] = numb;
        return num;
    }

    /**
     * 有符號的數值交換
     * @param numa
     * @param numb
     * @return
     */
    public static int[] exchange4(int numa, int numb) {
        int[] num = new int[2];
        // 不同符號
        if (numa * numb <= 0) {
            numa = numa + numb;
            numb = numa - numb;
            numa = numa - numb;
        } else {

            numa = numa - numb;
            numb = numa + numb;
            numa = numb - numa;
        }
        num[0] = numa;
        num[1] = numb;
        return num;
    }
}

另外不使用異或的方法存在一些問題,例如①使用中間變量,這種方法需要利用額外的空間存儲變量,②使用加減法或乘除法,可能造成溢出。
補充問題:在一個數組中找出唯一一個與數組內任何元素都不相等的一個元素
答:可以使用異或運算來達到目的,因爲問題的前提是數組內只含有一個不與其他元素重複的元素,那麼我們便可以利用異或運算的特點來解決此問題。因爲相同的元素經過異或運算的結果是0,那麼我們**遍歷數組內所有的元素,它們進行異或運算,最終得出的結果便是那個唯一不與其他元素重複的元素。**時間複雜度爲O(N)。

  1. 如何多線程改進K大頂堆或小頂堆?
    答:堆排序,是利用堆結構進行排序,一般是利用最大堆,即根節點比左右兩個子樹的節點都大,具體算法步驟如下。
    1、創建堆
    首先將數組中的元素調整成堆(對應下面程序中的createHeap(List list)方法)。創建堆就是從樹中最後一個非葉子節點(下標爲(n-1)/2)開始調整數組中元素的位置,使以這個非葉子節點爲根的子樹滿足堆的結構。即依次將以(n-1)/2、(n-1)/2-1、(n-1)/2-2、…、1、0爲根的子樹調整爲堆。
    創建堆主要用了shiftDown操作(對應下面的shiftDown(List list,int parent,int end)方法)。該操作是不斷將小元素下調的過程,如果根元素小於左右兩個子樹的較大者,那麼根元素就要跟這個較大者進行交換,直到到達葉子節點爲止。siftDown操作每下降一層要比較兩次:1)選擇左右子樹的較大者,2)將較大者跟父結點比較。下降的最大高度即爲父節點的高度。由結論:高度爲h的滿二叉樹的所有節點的高度和爲n-h-1,高度從0開始,葉子節點高度爲0,此結論可由數學歸納法證明,可知最壞情況的比較次數約爲2(n-h-1),即創建堆的複雜度度爲O(n)。
    2、排序
    當將數組調整成堆之後,由堆的定義可知,樹的根就是最大的元素,這時我們可以將根刪除,並將最後一個元素放到根的位置,然後將樹重新調整爲堆。重新調整爲堆之後,根節點又爲最大的元素,再刪除,再調整,直到元素全部排序。(這裏實際上是將最後一個元素和根元素交換,從此以後最後一個元素不在參與樹的重新調整,即已經排好序的元素不再參與樹的調整)。
    shiftDown操作的時間複雜度爲O(lgn),即樹的高度,排序過程中一共進行了n-1的shiftDown操作,所以可以粗略的推斷出堆排序的時間複雜度爲O(nlgn)。空間複雜度爲O(1)
    3、優化
    堆中從根節點到葉節點的一條路徑是有序的,最大堆是降序,我們在shiftDown操作時,實際上是在找根節點在某條路徑上的一個插入位置,可以借鑑二分的思想,我們可以一次下降到當前高度h的一半的位置(在下降的過程中要將沿途的較大的子節點上移,這樣在h/2高度形成空位),即h/2,再比較在一半高度h/2的節點與根節點的大小,如果比根節點大則繼續尋找當前一半高度位置的元素即h/4,依次類推,如果當前高度的元素比根節點小,我們就引入shiftUp操作,即將根節點再上移,但由前面的分析可知,上移的次數是有限的,這樣就將shiftDown的複雜度變爲O(lglgn),堆排序的總體複雜度變爲O(n*lglgn)。
    代碼如下:
import java.util.Arrays;
import java.util.List;

/**
 * 創建最大堆,並進行排序
 */
public class HeapSortAdvance {
    public static void main(String[] args) {
        HeapSortAdvance hsa = new HeapSortAdvance();
        List<Integer> list = null;
        list = Arrays.asList(3, 0, 23, 6, 5, 43, 23, 566, 67, 34, 2, 56, 98, 42, 49);
        hsa.createHeap(list);
        hsa.sort(list);
        for (int i = 0; i < list.size(); i++) {
            System.out.println(list.get(i) + " ");
        }
    }

    /**
     * 創建堆,從第一個非孩子結點開始調整,創建一個最大堆
     *
     * @param list
     */
    private List<Integer> createHeap(List<Integer> list) {
        int n = list.size();
        for (int k = (n - 1) / 2; k >= 0; k--) {
            shiftDown(list, k, n - 1);
        }
        return list;
    }

    /**
     * 將以parent爲根節點的二叉樹調整爲堆
     *
     * @param list
     * @param parent
     * @param end
     */
    private void shiftDown(List<Integer> list, int parent, int end) {
        boolean flag = true;
        while (parent * 2 + 1 <= end && flag) { // 如果存在左孩子就進行調整
            int lchild = parent * 2 + 1;
            int max = 0;
            if (lchild + 1 <= end) { //表示存在右孩子
                int rchild = lchild + 1;
                max = list.get(lchild) > list.get(rchild) ? lchild : rchild;
            } else {
                max = lchild;
            }
            if (list.get(max) > list.get(parent)) { //有孩子比父親大
                swap(list, max, parent);
                parent = max;
            } else {
                flag = false; //表示孩子都比父親大,不需要調整了
            }
        }
    }

    private void swap(List<Integer> list, int m, int n) {
        int temp = list.get(n);
        list.set(n, list.get(m));
        list.set(m, temp);
    }

    public void sort(List<Integer> heap) { //排序主過程
        int n = heap.size();
        int rmd = n - 1;
        while (rmd > 0) {
            swap(heap, 0, rmd);
            shiftDown(heap, 0, rmd - 1);
            rmd--;
        }
    }
}

  1. 多個有序數組合併爲一個
    答:首先考慮合併兩個有序數組的情況。基本思路如下:
    1)如果其中一個數組的元素均大於另一個數組的元素,則可以直接組合,不用拆分。
    即:其中一個數組的第一個元素大於或者小於另一個數組的最後一個元素
    2)若不滿足1)中的情況,則表明數組需要拆分,拆分的方法如下:
    ①拆分前,默認兩個數組以及最終輸出數組的索引均爲0;
    ② 兩個數組對應索引下的元素進行比較,小的一方放入最終數組中的當前索引下的位置,並使小的一方數組的索引+1;
    ③檢查是否有數組已經遍歷完畢,若有(即該數組的元素已經完全分配到結果數組中),則將另一個數組的剩餘元素依次放入最終數組中,直接輸出即可。
    ④最終數組的索引+1,並重復②,直到兩個數組均完成索引任務。
    代碼如下:
public class MyClass {
    public static void main(String[] args) {
        int[] num1 = new int[]{1, 2, 4, 6, 7, 123, 411, 5334, 1414141, 1314141414};
        int[] num2 = new int[]{0, 2, 5, 7, 89, 113, 5623, 6353, 134134};
        //變量用於存儲兩個集合應該被比較的索引(存入新集合就加一)
        int a = 0;
        int b = 0;
        int[] num3 = new int[num1.length + num2.length];
        for (int i = 0; i < num3.length; i++) {
            if (a < num1.length && b < num2.length) {   //兩數組都未遍歷完,相互比較後加入
                if (num1[a] > num2[b]) {
                    num3[i] = num2[b];
                    b++;
                } else {
                    num3[i] = num1[a];
                    a++;
                }
            } else if (a < num1.length) {   //num2已經遍歷完,無需比較,直接將剩餘num1加入
                num3[i] = num1[a];
                a++;
            } else if (b < num2.length) {    //num1已經遍歷完,無需比較,直接將剩餘num2加入
                num3[i] = num2[b];
                b++;
            }
        }
        System.out.println("排序後:" + Arrays.toString(num3));
    }
}

然後,我們考慮如何合併K個有序數組。最簡單的方法是創建一個N大小的數組,然後把所有的數字拷貝進去,然後在進行時間複雜度爲O(NlogN)的排序算法,這樣總體時間複雜度爲O(NlogN)。除此之外,可以利用最小堆完成時間複雜度爲O(NlogK),具體過程如下:
①創建一個大小爲N的數組保存最後的結果
②數組本身已經是從大到小的排序,所以我們只需要創建一個大小爲K 的最小堆,堆中的初始元素爲K個數組中的每個數組的第一個元素,每次從堆中取出最小元素(堆頂元素),並將其存入輸出數組中,將堆頂元素所在行的下一個元素加入堆,重新排列堆頂元素,時間複雜度爲logK,總計N個元素,所以總體時間複雜度是NlogK
在O(NlogK)的時間複雜度內完成:

  • N是所有數組包含的整數個數
  • k是數組的個數
    除此之外,還可以使用**分治法下的歸併。**合併兩個有序數組直接使用merge即可,而合併k個可以將其看作兩部分,左邊合併後的結果與後邊合併後的結果,然後再merge。遞歸的出口是lr,代表只有一個有序數組直接返回,l+1r,代表剛好有兩個有序數組直接merge。
    代碼如下:
package DataStructure;

import java.util.Arrays;
import java.util.Comparator;
import java.util.PriorityQueue;
import java.util.Queue;

class Element {
    public int row, col, val;

    Element(int row, int col, int val) {
        this.row = row;
        this.col = col;
        this.val = val;
    }
}

public class MergeTwoArray {
    public static void main(String[] args) {
        //定義K個一維數組
        int[][] array = {{1, 4, 7, 8, 12, 16, 20}, {2, 5, 7, 9, 10, 25}, {3, 6, 11, 13, 17, 18}};
        int[] merged = mergedKSortedArray(array); // 使用最小堆
        int[] merged2 = mergedKSortedArray2(array); //使用歸併法
        System.out.println(Arrays.toString(merged));
        System.out.println(Arrays.toString(merged2));
    }

    /**
     * 方法1:使用最小堆
     * @param array
     * @return
     */
    private static int[] mergedKSortedArray(int[][] array) {
        if (array == null) {
            return new int[0];
        }
        int totalSize = 0;
        //默認由小到大排序
        Queue<Element> queue = new PriorityQueue<Element>(array.length, ElementComparator);
        //初始化
        //把每一行的第一個元素加入PriorityQueue
        //統計元素總量
        for (int i = 0; i < array.length; i++) {
            //當前行長度不爲0
            if (array[i].length > 0) {
                Element element = new Element(i, 0, array[i][0]);
                queue.add(element);
                totalSize += array[i].length;
            }
        }

        int[] result = new int[totalSize];
        int index = 0;
        while (!queue.isEmpty()) {
            Element element = queue.poll();
            result[index++] = element.val;
            //當前結點被PriorityQueue跑出來後,當前行的第二個結點加入PriorityQueue
            if (element.col + 1 < array[element.row].length) {
                element.col += 1;
                element.val = array[element.row][element.col];
                queue.add(element);
            }
        }
        return result;
    }

    private static Comparator<Element> ElementComparator = new Comparator<Element>() {
        @Override
        public int compare(Element left, Element right) {
            return left.val - right.val;
        }
    };

    /**
     * 方法2:使用分治法下的歸併
     * @param array
     * @return
     */
    private static int[] mergedKSortedArray2(int[][] array) {
        if (array == null) {
            return new int[0];
        }
        return helper(array, 0, array.length - 1);
    }

    private static int[] helper(int[][] array, int l, int r) {
        if (l == r) {
            return array[l];
        }
        if (l + 1 == r) {
            return merged2Array(array[l], array[r]);
        }
        int mid = l + (r - l) / 2;
        int[] left = helper(array, l, mid);
        int[] right = helper(array, mid + 1, r);

        return merged2Array(left, right);
    }

    private static int[] merged2Array(int[] a, int[] b) {
        int[] res = new int[a.length + b.length];
        int i = 0, j = 0;
        for (int k = 0; k < res.length; k++) {
            if (i >= a.length) {
                res[k] = b[j++];
            } else if (j >= b.length) {
                res[k] = a[i++];
            } else if (a[i] < b[j]) {
                res[k] = a[i++];
            } else {
                res[k] = b[j++];
            }
        }
        return res;
    }
}

  1. 順時針打印數組
    答:順時針打印,分爲四個步驟:從左到右,從上到下,從右到左,從下到上。問題的關鍵在於如果打印中矩陣只剩下一個數字,只有一行,只有一列的情況,該如何控制?這裏設置四個參數:left、right、top、buttom,邊界處理過程如下:
    ①從左到右,第一步是需要的,判斷left是否小於right;
    ②從上到下,終止行要大於起始行,即buttom>top
    ③從右到左,至少有兩行兩列纔會執行,也就是right>left&&buttom>top
    ④從下到上,至少有三行兩列纔會執行第四步;即right>left&&buttom-1>top.
    代碼如下:
package DataStructure;

import java.util.Arrays;

public class SortPringMatrix {
    public static void main(String[] args) {
        int[][] matrix = {
                {57, 50, 59, 18, 31, 13},
                {67, 86, 93, 86, 4, 9},
                {38, 98, 83, 56, 82, 90},
                {66, 50, 67, 11, 7, 69},
                {20, 58, 55, 24, 66, 10},
                {43, 26, 65, 0, 64, 28},
                {62, 86, 38, 19, 37, 98}
        };
        int[] res = printMatrix(matrix);
        System.out.println(Arrays.toString(res));
    }

    private static int[] printMatrix(int[][] matrix) {
        int rows = matrix.length;
        int cols = matrix[0].length;
        if (matrix == null || rows == 0 || cols == 0) {
            System.out.println("");
        }
        int left = 0;
        int right = cols - 1;
        int top = 0;
        int buttom = rows - 1;
        int index = 0;
        int[] result = new int[rows * cols];
        while (left <= right && top <= buttom) {
            // 從左到右
            if (left <= right) {
                for (int i = left; i <= right; i++) {
                    //System.out.println(matrix[top][i]);
                    result[index++] = matrix[top][i];
                }
            }
            //從上到下
            if (buttom > top) {
                for (int i = top + 1; i <= buttom; i++) {
                    //System.out.println(matrix[i][right]);
                    result[index++] = matrix[i][right];
                }
            }
            //從右到左
            if (right > left && buttom > top) {
                for (int i = right - 1; i >= left; i--) {
                    //System.out.println(matrix[buttom][i]);
                    result[index++] = matrix[buttom][i];
                }
            }
            //從下到上
            if (buttom - 1 > top && right > left) {
                for (int i = buttom - 1; i > top; i--) {
                    //System.out.println(matrix[i][left]);
                    result[index++] = matrix[i][left];
                }
            }
            left++;
            right--;
            top++;
            buttom--;
        }
        return result;
    }
}

  1. 寫一個二分查找算法。對於一個有序數組,我們通常採用二分查找的方式來定位某一元素,請編寫二分查找的算法,在數組中查找指定元素。
    給定一個整數數組A及它的大小n,同時給定要查找的元素val,請返回它在數組中的位置(從0開始),若不存在該元素,返回-1。若該元素出現多次,請返回第一次出現的位置。
    測試樣例:
    [1,3,5,7,9],5,3
    返回:1
    答:代碼如下:
package DataStructure;

public class BinarySearch {
    public static void main(String[] args) {
        int[] A = {4,4,10,21};
        int n = 4;
        int val = 4;
        int res = binarySearch(A,n,val);
        System.out.println(res);
    }

    private static int binarySearch(int[] A, int n, int val) {
        int index = -1;
        if (n<=0||A==null){
            return index;
        }
        int mid = 0, left = 0, right = n-1;
        while (left<=right){
            mid = (left+right)/2;
            if (A[mid]>val){
                right = mid-1;
            }else if (A[mid]<val){
                left = mid+1;
            }else { // 找到第一個匹配的值
                while (mid>=0&&A[mid]==val){
                    mid--;
                }
                index = mid+1; //當不滿足時跳出循環,因此mid+1
                break;
            }
        }
        return index;
    }
}

相關問題:LeetCode278:第一個錯誤的版本
你是產品經理,目前正在帶領一個團隊開發新的產品。不幸的是,你的產品的最新版本沒有通過質量檢測。由於每個版本都是基於之前的版本開發的,所以錯誤的版本之後的所有版本都是錯的。
假設你有 n 個版本 [1, 2, …, n],你想找出導致之後所有版本出錯的第一個錯誤的版本。
你可以通過調用 bool isBadVersion(version) 接口來判斷版本號 version 是否在單元測試中出錯。實現一個函數來查找第一個錯誤的版本。你應該儘量減少對調用 API 的次數。
示例:

給定 n = 5,並且 version = 4 是第一個錯誤的版本。

調用 isBadVersion(3) -> false
調用 isBadVersion(5) -> true
調用 isBadVersion(4) -> true

所以,4 是第一個錯誤的版本。 

答:在二分查找中,選取mid的方法一般爲mid=[(left+mid)/2]。如果使用的編程語言有整數溢出的情況(例如C++,Java),那麼可以用mid=left+[right-left]/2代替前者。
代碼如下:

private static int firstBadVersion(int n){
        int left = 1;
        int right = n;
        while (left<right){
            int mid = left+(right-left)/2;
            if (isBadVersion(mid)){
                right = mid;
            }else {
                left = mid+1;
            }
        }
        return left;
    }

注意,這裏更好的寫法是int mid = (left+right)>>>1;
補充問題:1) 爲什麼要left+(right-left)/2(答怕溢出);2) int 的最大值是?(2311=21474836472^{31}-1=2147483647);3) (right-left)/2會不會出現浮點數?(不會);4) 二分查找有什麼要求,數組有序,升序降序都可以嗎?(要求數組/序列滿足一定的有序性);5) 能不能寫個代碼讓升序降序都滿足?
答:首先判斷數組是否有序:升序或降序,然後根據判斷結果執行二分查找。
代碼如下:

package DataStructure;

public class BinarySearch {
    public static void main(String[] args) {
        int val = 4;
        int n = 9;
        // 升序
        int[] A = {1, 2, 4, 4, 9, 10, 15, 17, 21};
        System.out.println(binarySearch(A, val));
        // 降序
        int[] B = {21, 17, 15, 10, 9, 4, 4, 3, 2};
        System.out.println(binarySearch(B, val));
        // 無序
        int[] C = {17, 21, 10, 15, 7, 9, 5, 6, 4};
        System.out.println(binarySearch(C, val));
    }

    /**
     * 二分查找
     *
     * @param array     :被查找的數組,默認有序
     * @param val:要查找的數
     * @return int: 下標位置
     */
    private static int binarySearch(int[] array, int val) {
        // 1.參數合法性判斷
        if (null == array || array.length == 0) {
            return -1;
        }
        // 2.判斷是否有序,以及升序或者降序
        boolean flag1 = false, flag2 = false;
        for (int i = 0; i < array.length - 1; i++) {
            if (array[i] == Math.min(array[i], array[i + 1])) { //升序
                flag1 = true;
            } else {
                flag1 = false;
                break;
            }
        }
        if (!flag1) {
            for (int i = 0; i < array.length - 1; i++) {
                if (array[i] == Math.max(array[i], array[i + 1])) { //降序
                    flag2 = true;
                } else {
                    flag2 = false;
                    break;
                }
            }
        }
        if (flag1 || flag2) {
            return binarySearch(array, array.length, val, flag1);
        }
        return -1;
    }

    private static int binarySearch(int[] array, int n, int val, boolean up) {
        int index = -1;
        int mid = 0, left = 0, right = n - 1;
        while (left <= right) {
            mid = left + (right - left) / 2;
            if (array[mid] > val) {
                if (up) {
                    right = mid - 1; //升序左移
                } else {
                    left = mid + 1; //降序右移
                }
            } else if (array[mid] < val) {
                if (up) {
                    left = mid + 1;// 升序右移
                } else {
                    right = mid - 1; //降序右移
                }
            } else { // 找到第一個匹配的值
                while (mid >= 0 && array[mid] == val) {
                    mid--;
                }
                index = mid + 1;
                break;
            }
        }
        return index;
    }
}

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章