面試 18:復(fù)雜鏈表的復(fù)制(劍指 Offer 第 26 題)

面試 18:復(fù)雜鏈表的復(fù)制(劍指 Offer 第 26 題)

在上一篇推文中丈攒,我們留下的習(xí)題是來(lái)自《劍指 Offer》 的面試題 26:復(fù)雜鏈表的復(fù)制镣陕。

請(qǐng)實(shí)現(xiàn)復(fù)雜鏈表的復(fù)制坝撑,在復(fù)雜鏈表中厉碟,每個(gè)結(jié)點(diǎn)除了 next 指針指向下一個(gè)結(jié)點(diǎn)外查库,還有一個(gè) sibling 指向鏈表中的任意結(jié)點(diǎn)或者 NULL碍庵。比如下圖就是一個(gè)含有 5 個(gè)結(jié)點(diǎn)的復(fù)雜鏈表映企。

image.png

提前想好測(cè)試用例

依舊是我們熟悉的第一步,先想好我們的測(cè)試用例:

  1. 輸入一個(gè) null 静浴,期望什么也不輸出堰氓;
  2. 輸入一個(gè)結(jié)點(diǎn),sibling 指向自身苹享,期望打印符合題干的值双絮;
  3. 輸入多個(gè)結(jié)點(diǎn)浴麻,部分 sibling 指向 null,期望打印符合題干的值囤攀。

思考程序邏輯

測(cè)試用例思考完畢软免,自然是開(kāi)始思考我們的測(cè)試邏輯了,在思考的過(guò)程中焚挠,我們不妨嘗試和面試官進(jìn)行溝通膏萧,這樣可以避免我們走不少?gòu)澛罚乙踩菀捉o面試官留下一個(gè)善于思考和溝通的好印象蝌衔。

極易想到的邏輯是榛泛,我們先復(fù)制我們傳統(tǒng)的單鏈表,然后再遍歷單鏈表噩斟,復(fù)制 sibling 的指向曹锨。

假設(shè)鏈表中有個(gè)結(jié)點(diǎn) A,A 的 sibling 指向結(jié)點(diǎn) B剃允,這個(gè) B 可能在 A 前面也可能在 A 后面沛简,所以我們唯一的辦法只有從頭結(jié)點(diǎn)開(kāi)始遍歷。對(duì)于一個(gè)含有 n 個(gè)結(jié)點(diǎn)的鏈表斥废,由于定位每個(gè)結(jié)點(diǎn)的 sibling 都需要從鏈表頭結(jié)點(diǎn)開(kāi)始經(jīng)過(guò) O(n) 步才能找到椒楣,因此這種方法的時(shí)間復(fù)雜度是 O(n2)。

當(dāng)我們告知面試官我們這樣的思路的時(shí)候牡肉,面試官告訴我們撒顿,他期待的并不是這樣的算法,這樣的算法時(shí)間復(fù)雜度也太高了荚板,希望能有更加簡(jiǎn)單的方式。

得到了面試官的訴求吩屹,我們?cè)賮?lái)看看我們前面的想法時(shí)間都花在哪兒去了跪另。

很明顯,我們上面的想法在定位 sibling 指向上面花了大量的時(shí)間煤搜,我們可以嘗試在這上面進(jìn)行優(yōu)化免绿。我們還是分為兩步:第一步仍然是先復(fù)制原始鏈表上的每個(gè)結(jié)點(diǎn) N 創(chuàng)建 N1,然后把這些創(chuàng)建出來(lái)的結(jié)點(diǎn)用 next 連接起來(lái)擦盾。同時(shí)我們把 <N,N1> 的配對(duì)信息放在一個(gè)哈希表中嘲驾。第二步是設(shè)置復(fù)制鏈表的 sibling 指向,如果原始鏈表中有 N 指向 S迹卢,那么我們的復(fù)制鏈表中必然存在 N1 指向 S1 辽故。由于有了哈希表,我們可以用 O(1) 的時(shí)間腐碱,根據(jù) S 找到 S1誊垢。

這樣的方法降低了時(shí)間成本,我們高興地與面試官分享我們的想法,卻被面試官指出喂走,這樣的想法雖然把時(shí)間復(fù)雜度降低到了 O(n)殃饿,但卻由于哈希表的存在,需要 O(n) 的空間芋肠,而他所期望的方法是不占用任何輔助空間的乎芳。

接下來(lái)我們?cè)贀Q一下思路,不用輔助空間帖池,我們卻要用更少的實(shí)際解決 sibling 的指向問(wèn)題奈惑。

我們前面似乎對(duì)于指向都采用過(guò)兩個(gè)指針的方法,這里似乎可以用類似的處理方式處理碘裕。

我們不妨利用原有鏈表對(duì)每個(gè)結(jié)點(diǎn) N 在后面直接在后面創(chuàng)建 N1携取,這樣相當(dāng)于我們擴(kuò)長(zhǎng)原始鏈表長(zhǎng)度為現(xiàn)有鏈表的 2 倍,奇數(shù)位置的結(jié)點(diǎn)連接起來(lái)是原始鏈表帮孔,偶數(shù)位置的結(jié)點(diǎn)連接起來(lái)就是我們的復(fù)制鏈表雷滋。

開(kāi)始編寫代碼

我們先完成第一部分的代碼。根據(jù)原始鏈表的每個(gè)結(jié)點(diǎn) N 文兢,創(chuàng)建 N1晤斩,并把 N 的 next 指向 N1,N1 的 next 指向 N 的 next姆坚。

private static void cloneNodes(Node head) {
    Node node = null;
    while (head != null) {
        // 先新建結(jié)點(diǎn)
        node = new Node(head.data);
        // 再把head 的 next 指向 node 的 next
        node.next = head.next;
        // 然后把 node 作為 head 的 next
        head.next = node;
        // 最后遍歷條件
        head = node.next;
    }
}

上面完成了復(fù)制結(jié)點(diǎn)澳泵,下面我們需要編寫 sibling 的指向復(fù)制。

我們的思想是:當(dāng) N 執(zhí)行 S兼呵,那么 N1 就應(yīng)該指向 S1兔辅,即 N.next.sibling = N.sibling.next;

private static void connectNodes(Node head) {
    while (head != null) {
        if (head.sibling != null) {
            //如果 當(dāng)前結(jié)點(diǎn)的 sibling 不為 null,那就把它后面的復(fù)制結(jié)點(diǎn)指向當(dāng)前sibling指向的下一個(gè)結(jié)點(diǎn)
            head.next.sibling = head.sibling.next;
        }
        // 遍歷
        head = head.next.next;
    }
}

最后我們只需要拿出原本的鏈表(奇數(shù))和復(fù)制的鏈表(偶數(shù))即可击喂。

private static Node reconnectList(Node head) {
    if (head == null)
        return null;
    // 用于存放復(fù)制鏈表的頭結(jié)點(diǎn)
    Node cloneHead = head.next;
    // 用于記錄當(dāng)前處理的結(jié)點(diǎn)
    Node temp = cloneHead;
    // head 的 next 還是要指向原本的 head.next
    // 實(shí)際上現(xiàn)在由于復(fù)制后维苔,應(yīng)該是 head.next.next,即cloneHead.next
    head.next = cloneHead.next;
    // 指向新的被復(fù)制結(jié)點(diǎn)
    head = head.next;
    while (head != null) {
        // temp 代表的是復(fù)制結(jié)點(diǎn)
        // 先進(jìn)行賦值
        temp.next = head.next;
        // 賦值結(jié)束應(yīng)該給 next 指向的結(jié)點(diǎn)賦值
        temp = temp.next;
        // head 的下一個(gè)結(jié)點(diǎn)應(yīng)該指向被賦值的下一個(gè)結(jié)點(diǎn)
        head.next = temp.next;
        head = temp.next;
    }
    return cloneHead;
}

合并后的最終代碼就是:

public class Test18 {

    private static class Node {
        int data;
        Node next;
        Node sibling;

        Node(int data) {
            this.data = data;
        }
    }

    private static Node complexListNode(Node head) {
        if (head == null)
            return null;
        // 第一步懂昂,復(fù)制結(jié)點(diǎn)介时,并用 next 連接
        cloneNodes(head);
        // 第二步,把 sibling 也復(fù)制起來(lái)
        connectNodes(head);
        // 第三步凌彬,返回偶數(shù)結(jié)點(diǎn)沸柔,連接起來(lái)就是復(fù)制的鏈表
        return reconnectList(head);
    }

    private static void cloneNodes(Node head) {
        Node node = null;
        while (head != null) {
            // 先新建結(jié)點(diǎn)
            node = new Node(head.data);
            // 再把head 的 next 指向 node 的 next
            node.next = head.next;
            // 然后把 node 作為 head 的 next
            head.next = node;
            // 最后遍歷條件
            head = node.next;
        }
    }

    private static void connectNodes(Node head) {
        while (head != null) {
            if (head.sibling != null) {
                // 如果 當(dāng)前結(jié)點(diǎn)的 sibling 不為 null,那就把它后面的復(fù)制結(jié)點(diǎn)指向當(dāng)前sibling指向的下一個(gè)結(jié)點(diǎn)
                head.next.sibling = head.sibling.next;
            }
            // 遍歷
            head = head.next.next;
        }
    }

    private static Node reconnectList(Node head) {
        if (head == null)
            return null;
        // 用于存放復(fù)制鏈表的頭結(jié)點(diǎn)
        Node cloneHead = head.next;
        // 用于記錄當(dāng)前處理的結(jié)點(diǎn)
        Node cloneNode = cloneHead;
        // head 的 next 還是要指向原本的 head.next
        // 實(shí)際上現(xiàn)在由于復(fù)制后,應(yīng)該是 head.next.next铲敛,即cloneHead.next
        head.next = cloneHead.next;
        // 因?yàn)槲覀兊谝粋€(gè)結(jié)點(diǎn)已經(jīng)拆分了褐澎,所以需要指向新的被復(fù)制結(jié)點(diǎn)才可以開(kāi)始循環(huán)
        head = head.next;
        while (head != null) {
            // cloneNode 代表的是復(fù)制結(jié)點(diǎn)
            // 先進(jìn)行賦值
            cloneNode.next = head.next;
            // 賦值結(jié)束應(yīng)該給 next 指向的結(jié)點(diǎn)賦值
            cloneNode = cloneNode.next;
            // head 的下一個(gè)結(jié)點(diǎn)應(yīng)該指向被賦值的下一個(gè)結(jié)點(diǎn)
            head.next = cloneNode.next;
            head = cloneNode.next;
        }
        return cloneHead;
    }


    public static void main(String[] args) {
        Node head1 = new Node(1);
        Node node2 = new Node(2);
        Node node3 = new Node(3);
        Node node4 = new Node(4);
        Node node5 = new Node(5);
        head1.next = node2;
        node2.next = node3;
        node3.next = node4;
        node4.next = node5;
        node5.next = null;
        head1.sibling = node4;
        node2.sibling = null;
        node3.sibling = node5;
        node4.sibling = node2;
        node5.sibling = head1;

        print(head1);
        Node root = complexListNode(head1);
        System.out.println();
        print(head1);
        print(root);
        System.out.println();
        System.out.println(isSameLink(head1, root));
    }

    private static boolean isSameLink(Node head, Node root) {
        while (head != null && root != null) {
            if (head == root) {
                head = head.next;
                root = root.next;
            } else {
                return false;
            }
        }
        return head == null && root == null;
    }

    private static void print(Node head) {
        Node temp = head;
        while (head != null) {
            System.out.print(head.data + "->");
            head = head.next;
        }
        System.out.println("null");
        while (temp != null) {
            System.out.println(temp.data + "=>" + (temp.sibling == null ? "null" : temp.sibling.data));
            temp = temp.next;
        }
    }
}

驗(yàn)證測(cè)試用例

寫畢代碼,我們驗(yàn)證我們的測(cè)試用例伐蒋。

  1. 輸入一個(gè) null 乱凿,也不會(huì)輸出顽素,測(cè)試通過(guò);
  2. 輸入一個(gè)結(jié)點(diǎn)徒蟆,sibling 指向自身胁出,測(cè)試通過(guò);
  3. 輸入多個(gè)結(jié)點(diǎn)段审,部分 sibling 指向 null全蝶,測(cè)試通過(guò)。

課后習(xí)題

下一次推文的習(xí)題來(lái)自于《劍指 Offer》第 29 題:數(shù)組中超過(guò)一半的數(shù)字

面試題:數(shù)組中有一個(gè)數(shù)字出現(xiàn)的次數(shù)超過(guò)數(shù)組長(zhǎng)度的一半寺枉,請(qǐng)找出這個(gè)數(shù)字并輸出抑淫。比如 {1,2,3,2,2,2,1} 中 2 的次數(shù)是 4,數(shù)組長(zhǎng)度為 7姥闪,所以輸出 2始苇。要求不能修改輸入的數(shù)組。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末筐喳,一起剝皮案震驚了整個(gè)濱河市催式,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌避归,老刑警劉巖荣月,帶你破解...
    沈念sama閱讀 219,110評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異梳毙,居然都是意外死亡哺窄,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,443評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門账锹,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)萌业,“玉大人,你說(shuō)我怎么就攤上這事奸柬⊙拾祝” “怎么了?”我有些...
    開(kāi)封第一講書人閱讀 165,474評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵鸟缕,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我排抬,道長(zhǎng)懂从,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書人閱讀 58,881評(píng)論 1 295
  • 正文 為了忘掉前任蹲蒲,我火速辦了婚禮番甩,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘届搁。我一直安慰自己缘薛,他們只是感情好窍育,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,902評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著宴胧,像睡著了一般漱抓。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上恕齐,一...
    開(kāi)封第一講書人閱讀 51,698評(píng)論 1 305
  • 那天乞娄,我揣著相機(jī)與錄音,去河邊找鬼显歧。 笑死仪或,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的士骤。 我是一名探鬼主播范删,決...
    沈念sama閱讀 40,418評(píng)論 3 419
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼拷肌!你這毒婦竟也來(lái)了到旦?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書人閱讀 39,332評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤廓块,失蹤者是張志新(化名)和其女友劉穎厢绝,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體带猴,經(jīng)...
    沈念sama閱讀 45,796評(píng)論 1 316
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡昔汉,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,968評(píng)論 3 337
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了拴清。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片靶病。...
    茶點(diǎn)故事閱讀 40,110評(píng)論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖口予,靈堂內(nèi)的尸體忽然破棺而出娄周,到底是詐尸還是另有隱情,我是刑警寧澤沪停,帶...
    沈念sama閱讀 35,792評(píng)論 5 346
  • 正文 年R本政府宣布煤辨,位于F島的核電站,受9級(jí)特大地震影響木张,放射性物質(zhì)發(fā)生泄漏众辨。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,455評(píng)論 3 331
  • 文/蒙蒙 一舷礼、第九天 我趴在偏房一處隱蔽的房頂上張望鹃彻。 院中可真熱鬧,春花似錦妻献、人聲如沸蛛株。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 32,003評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)谨履。三九已至欢摄,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間屉符,已是汗流浹背剧浸。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 33,130評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留矗钟,地道東北人唆香。 一個(gè)月前我還...
    沈念sama閱讀 48,348評(píng)論 3 373
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像吨艇,于是被迫代替她去往敵國(guó)和親躬它。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,047評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容