数据结构 - 链表 - 面试中常见的链表算法题
數據結構 - 鏈表 - 面試中常見的鏈表算法題
數據結構是面試中必定考查的知識點,面試者需要掌握幾種經典的數據結構:線性表(數組、鏈表)、棧與隊列、樹(二叉樹、二叉查找樹、平衡二叉樹、紅黑樹)、圖。
本文主要介紹線性表中的常見的鏈表數據結構。包括
- 概念簡介
- 鏈表節(jié)點的數據結構(Java)
- 常見的鏈表算法題(Java)。
概念簡介
如果對鏈表概念已經基本掌握,可以跳過該部分,直接查看常見鏈表算法題。
線性表基本概念
鏈表是一種線性表,因此我們首先了解一下什么是線性表:
線性表是最常用且最簡單的一種數據結構,它是n個數據元素的有限序列。實現線性表的方式一般有兩種:
- 使用數組存儲線性表的元素,即用一組連續(xù)的存儲單元依次存儲線性表的數據元素。
- 使用鏈表存儲線性表的元素,即用一組任意的存儲單元存儲線性表的數據元素(存儲單元可以是連續(xù)的,也可以是不連續(xù)的)。
鏈表基本概念
鏈表是一種物理存儲單元上非連續(xù)、非順序的存儲結構,數據元素的邏輯順序是通過鏈表中的指針鏈接次序實現的。
鏈表由一系列結點(鏈表中每一個元素稱為結點)組成,結點可以在運行時動態(tài)生成。每個結點包括兩個部分:一個是存儲數據元素的數據域,另一個是存儲下一個結點地址的指針域。
相比于線性表順序結構(數組),操作復雜。由于不必須按順序存儲,鏈表在插入的時候可以達到O(1)的復雜度,比另一種線性表順序表快得多,但是查找一個節(jié)點或者訪問特定編號的節(jié)點則需要O(n)的時間,而線性表和順序表相應的時間復雜度分別是O(logn)和O(1)。
使用鏈表結構可以克服數組鏈表需要預先知道數據大小的缺點,鏈表結構可以充分利用計算機內存空間,實現靈活的內存動態(tài)管理。但是鏈表失去了數組隨機讀取的優(yōu)點,同時鏈表由于增加了結點的指針域,空間開銷比較大。
鏈表最明顯的好處就是,常規(guī)數組排列關聯項目的方式可能不同于這些數據項目在記憶體或磁盤上順序,數據的存取往往要在不同的排列順序中轉換。鏈表允許插入和移除表上任意位置上的節(jié)點,但是不允許隨機存取。
鏈表有很多種不同的類型:單向鏈表,雙向鏈表以及循環(huán)鏈表。
數組和鏈表的區(qū)別
- 數組是同類型的連續(xù)的一個存儲空間,鏈表是不連續(xù)的,其元素結點是一個結構體。
- 數組是在棧中分配的,即數組大小在編譯時就已經確定,即內存是靜態(tài)分配的;鏈表是在堆中分配的,運行過程才具體分配,即鏈表是動態(tài)分配內存。
- 數組對于元素的查詢是通過下標直接索引,而鏈表是通過結點之間的鏈接一步一步進行遍歷。數組對元素的查詢時間復雜度是O(1),鏈表對元素的查詢時間復雜度是O(n).
- 數組對于元素的插入、刪除效率較低,需要進行挪位。而鏈表插入,刪除只需要操作相應位置上的指針即可。數組對元素的插入、刪除時間復雜度是O(n),鏈表對元素的插入、刪除時間復雜度是O(1)。
鏈表節(jié)點的數據結構
鏈表由一系列結點組成,結點可以在運行時動態(tài)生成。每個結點包括兩個部分:一個是存儲數據元素的數據域,另一個是存儲下一個結點地址的指針域。
定義節(jié)點為類:ListNode。具體實現如下:
public class ListNode {public int val; // 數據域public ListNode next; // 指針域public ListNode() {}public ListNode(int val) {this.val = val;}// 打印鏈表節(jié)點元素值public static void printList(ListNode head) {while (head != null) {System.out.print(head.val + "->");head = head.next;}System.out.println("null");}}常見的鏈表算法題(單鏈表)
1. 求單鏈表中結點的個數
/*** 1. 求單鏈表中節(jié)點的個數:* 注意檢查鏈表是否為空。時間復雜度為O(n)。這個比較簡單。* @param head 鏈表頭結點* @return 鏈表中節(jié)點的個數*/ public static int getLength(ListNode head) {if (head == null) {return 0;}int length = 0;ListNode current = head;while (current != null) { // 當前元素不為空length++;current = current.next;}return length; }2. 查找單鏈表中的倒數第k個結點(劍指offer,題15)
思路:這里需要聲明兩個指針:即兩個結點型的變量first和second,首先讓first和second都指向第一個結點,然后讓second結點往后挪k-1個位置,此時first和second就間隔了k-1個位置,然后整體向后移動這兩個節(jié)點,直到second節(jié)點走到最后一個結點的時候,此時first節(jié)點所指向的位置就是倒數第k個節(jié)點的位置。時間復雜度為O(n)。
/*** 2. 查找單鏈表中的倒數第k個結點(劍指offer,題15)* 時間復雜度為O(n)** 考慮k=0和k大于鏈表長度的情況* @param head 鏈表頭結點* @param k 倒數第k* @return 倒數第k個節(jié)點*/ public static ListNode findLastNode(ListNode head, int k){if (head == null || k <= 0) { // 輸入異常throw new RuntimeException("輸入參數格式不對...");}ListNode first = head; // 兩個指針ListNode second = head;for (int i = 0; i < k - 1; i++) {second = second.next;if (second == null) { // 說明k的值大于鏈表的長度throw new RuntimeException("k越界");}}// 兩個指針同時移動,second到達尾節(jié)點時,first是倒數第k個節(jié)點while (second.next != null) {first = first.next;second = second.next;}return first; }3. 查找單鏈表中的中間結點
面試官不允許你算出鏈表的長度,該怎么做呢?
思路:和上面的第2節(jié)一樣,也是設置兩個指針first和second,只不過這里是,兩個指針同時向前走,second指針每次走兩步,first指針每次走一步,直到second指針走到最后一個結點時,此時first指針所指的結點就是中間結點。
注意鏈表為空,鏈表結點個數為1和2的情況。時間復雜度為O(n)。
/*** 3. 查找單鏈表中的中間結點:* 上方代碼中,當n為偶數時,得到的中間結點是第n/2個結點。比如鏈表有6個節(jié)點時,得到的是第3個節(jié)點。* @param head 鏈表頭結點* @return 中間節(jié)點*/ public static ListNode findMidNode(ListNode head){if (head == null || head.next == null || head.next.next == null) {return null;}ListNode first = head;ListNode second = head;while (second.next != null && second.next.next != null) {first = first.next;second = second.next.next;}return first; }4. 合并兩個有序的單鏈表,合并之后的鏈表依然有序【出現頻率高】(劍指offer,題17)
這道題經常被各公司考察。例如:
鏈表1:1->2->3->4; 鏈表2:2->3->4->5; 合并后:1->2->2->3->3->4->4->5解題思路:挨個比較鏈表1和鏈表2。這個類似于歸并排序。
尤其要注意兩個鏈表都為空、和其中一個為空的情況。
只需要O(1)的空間。時間復雜度為O (max(len1,len2))。
5. 單鏈表的反轉:【出現頻率最高】(不使用額外的空間)
例如:
鏈表:1->2->3->4 反轉之后:4->3->2->1思路:從頭到尾遍歷原鏈表,每遍歷一個結點,將其摘下放在新鏈表的最前端。
注意鏈表為空和只有一個結點的情況。時間復雜度為O(n)。
6. 從尾到頭打印單鏈表
遞歸實現:用遞歸實現,但有個問題:當鏈表很長的時候,就會導致方法調用的層級很深,有可能造成棧溢出。
/** * 6.從尾到頭打印單鏈表 * 用遞歸實現,但有個問題:當鏈表很長的時候,就會導致方法調用的層級很深,有可能造成棧溢出。 * 注意鏈表為空的情況。時間復雜度為O(n) * @param head 鏈表頭結點 */ public static void reversePrint(ListNode head) {if (head == null) {return;}reversePrint(head.next);System.out.print(head.val + "->"); }非遞歸實現:對于這種顛倒順序的問題,我們應該就會想到棧,后進先出。顯式用棧,是基于循環(huán)實現的,代碼的魯棒性要更好一些。
注意鏈表為空的情況。時間復雜度為O(n)。
7. 判斷單鏈表是否有環(huán)
這里也是用到兩個指針,如果一個鏈表有環(huán),那么用一個指針去遍歷,是永遠走不到頭的。因此,我們用兩個指針去遍歷:first指針每次走一步,second指針每次走兩步,如果first指針和second指針相遇,說明有環(huán)。
時間復雜度為O (n)。
8. 取出有環(huán)鏈表中環(huán)的長度
思路:環(huán)的長度即為從開始到相遇處first走的步數。
9. 有環(huán)單鏈表中,取出環(huán)的起始點
思路:從相遇點開始,設置一個節(jié)點從頭開始,然后最終相遇的節(jié)點就是環(huán)的起始點。由上圖中的a=c可知。
/*** 9、單鏈表中,取出環(huán)的起始點:從相遇點開始,設置一個節(jié)點從頭開始,然后最終相遇的節(jié)點就是環(huán)的起始點。* @param head 鏈表頭結點* @return 鏈表中環(huán)的起始節(jié)點*/ public static ListNode getCycleStart(ListNode head) {if (head == null || head.next == null) {return null;}ListNode first = head; // 每次移動一步ListNode second = head; // 每次移動兩步while (second != null && second.next != null) { // 判斷空指針first = first.next;second = second.next.next;if (first == second) {ListNode temp = head;while (temp != second) {temp = temp.next;second = second.next;}return second;}}return null; }10. 判斷兩個單鏈表相交的第一個交點。 劍指offer,題37。
思路:先遍歷兩個鏈表得到長度差,讓長的鏈表先走長度差步,然后再同時走相遇的第一個節(jié)點就是返回結果。
/*** 10、判斷兩個單鏈表相交的第一個交點。 劍指offer,題37。* 先遍歷兩個鏈表得到長度差,讓長的鏈表先走長度差步,然后再同時走相遇的第一個節(jié)點就是返回結果。* 時間復雜度為:O(m+n)* @param head1 鏈表1頭結點* @param head2 鏈表2頭結點* @return 相交的第一個節(jié)點*/ public static ListNode meetNode(ListNode head1, ListNode head2){if (head1 == null || head2 == null) {return null;}int len1 = 0;int len2 = 0;ListNode temp1 = head1;ListNode temp2 = head2;while (temp1 != null) {len1++;temp1 = temp1.next;}while (temp2 != null) {len2++;temp2 = temp2.next;}int diff = Math.abs(len1 - len2);ListNode longHead = head1;ListNode shortHead = head2;if (len1 < len2) {longHead = head2;shortHead = head1;}for (int i = 0; i < diff; i++) {longHead = longHead.next;}while (longHead != null && shortHead != null && longHead != shortHead) {longHead = longHead.next;shortHead = shortHead.next;}return longHead; }11. 以 k 個節(jié)點為段,反轉單鏈表。
Reverse Nodes in k_Group,Leetcode上的算法題,第5題的高級變種
/*** 11、以 k 個節(jié)點為段,反轉單鏈表。* Reverse Nodes in k_Group,Leetcode上的算法題,第6題的高級變種* @param head 鏈表頭結點* @param k 每k個節(jié)點反轉* @return 反轉后的鏈表頭*/ public static ListNode reverseKGroup2(ListNode head, int k) {ListNode curr = null;int count = 0;while (curr != null && count != k) { // find the k+1 nodecurr = curr.next;count++;}if (count == k) { // if k+1 node is foundcurr = reverseKGroup2(curr, k); // reverse list with k+1 node as head// head - head-pointer to direct part,// curr - head-pointer to reversed part;while (count-- > 0) { // reverse current k-groupListNode tmp = head.next; // tmp - next head in direct parthead.next = curr; // preappending "direct" head to the reversed listcurr = head; // move head of reversed part to a new nodehead = tmp; // move "direct" head to the next node in direct part}head = curr;}return head; }總結
以上是生活随笔為你收集整理的数据结构 - 链表 - 面试中常见的链表算法题的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 排序算法 - 面试中的排序算法总结
- 下一篇: 数据结构 - 二叉树 - 面试中常见的二