數據結構之「數組「和「鏈表」

前言

本片文章重點在於討論數據結構之「數組」和「鏈表」java

數組

鏈表

優勢和缺點node

  • 優勢:數據存儲在「節點」(Node)中,作到真正的動態,不須要處理 固定容量的問題。
  • 缺點:喪失了隨機訪問的能力。

掌握鏈表須要牢記下面幾大技巧:面試

技巧一:掌握鏈表,想輕鬆寫出正確的鏈表代碼,須要理解指針獲引用的含義:編程

對於指針的理解,我總結了下面這句話:數組

將某個變量賦值給指針,實際上就是將這個變量的地址賦值給指針,或者反過來講,指針中存儲了這個變量的內存地址,指向了這個變量,經過指針就能找到這個變量。數據結構

在編寫代碼中咱們常常遇到 p->next  = q、p->next = p->next->next二者的含義:spa

① p->next = q:p結點中的next指針存儲了q結點的內存地址(p結點的next指針指向q)3d

② p->next = p->next->next:p結點的next指針儲存了p結點的下下一個結點的內存地址指針

技巧二:插入節點時要警戒指針丟失和內存泄漏調試

假設咱們但願在結點a和相鄰結點b之間插入結點x,假設當前指針p指向結點a,若是咱們將代碼編程下面的這個樣子,就會發生指針丟失和內存泄漏:

p->next = x;  //將 p 的next 指針指向x結點
x->next = p->next;  //將 x 結點的next指針指向 b 結點

初學者常常會在這兒犯錯,p->next 指針在完成第一步操做以後,已經再也不指向結點 b了,而是指向結點 x。第 2 行代碼至關於將 x 賦值給 x->next,本身指向本身。所以,整個鏈表也就斷成了兩半,從結點 b 日後的全部結點都沒法訪問到了,因此在咱們插入結點時,必定要注意操做順序,現將x結點的next指針指向b,再把結點a的next指針指向結點x,這樣纔不會丟失指針,致使內存泄漏,正確的刪除操做代碼應該是:

x->next = p->next;
p->next = x;

技巧三:利用哨兵簡化實現難度

回顧單鏈表的插入和刪除操做,但願在p結點以後插入一個新的結點,須要下面兩行代碼能夠搞定:

newNode->next = p->next;
p->next = newNode;

可是,當咱們要向一個空鏈表中插入第一個結點,剛剛的邏輯就不能用了。咱們須要進行下面這樣的特殊處理,其中 head 表示鏈表的頭結點。因此,從這段代碼,咱們能夠發現,對於單鏈表的插入操做,第一個結點和其餘結點的插入邏輯是不同的。

if(head == null){
   head = newNode;
}

再來看看刪除結點:

p->next = p->next->next;

可是,若是咱們要刪除鏈表中的最後一個結點,前面的刪除代碼就不 work 了。跟插入相似,咱們也須要對於這種狀況特殊處理。寫成代碼是這樣子的:

if(head->next == null){
   head = null;
}

從前面的一步一步分析,能夠看出,針對鏈表的插入、刪除操做,須要對插入第一個結點和刪除最後一個結點的狀況進行特殊處理,這樣的代碼實現起來很繁瑣,不簡潔,而且也容易由於考慮不全而出錯,如何解決這個問題呢?此時,技巧三的提到的哨兵就要登場了,哨兵,解決的是國家之間的邊界問題。同理,這裏說的哨兵也是解決「邊界問題」的,不直接參與業務邏輯。還記得如何表示一個空鏈表嗎?head=null 表示鏈表中沒有結點了。其中 head 表示頭結點指針,指向鏈表中的第一個結點。

若是咱們引入哨兵結點,在任什麼時候候,無論鏈表是否是空,head 指針都會一直指向這個哨兵結點。咱們也把這種有哨兵結點的鏈表叫帶頭鏈表。相反,沒有哨兵結點的鏈表就叫做不帶頭鏈表。

實際上,這種利用哨兵簡化編程難度的技巧,在不少代碼實現中都有用到,好比插入排序、歸併排序、動態規劃等。

技巧四:重點留意邊界條件處理

軟件開發中,代碼在一些邊界或者異常狀況下,最容易產生 Bug。鏈表代碼也不例外。要實現沒有 Bug 的鏈表代碼,必定要在編寫的過程當中以及編寫完成以後,檢查邊界條件是否考慮全面,以及代碼在邊界條件下是否能正確運行。

咱們常常檢查鏈表代碼是否正確的邊界條件有這樣幾個:

  • 若是鏈表爲空時,代碼是否能正常工做?
  • 若是鏈表只包含一個結點時,代碼是否能正常工做?
  • 若是鏈表只包含兩個結點時,代碼是否能正常工做?
  • 代碼邏輯在處理頭結點和尾結點的時候,是否能正常工做?

實際上,不光光是寫鏈表代碼,你在寫任何代碼時,也千萬不要只是實現業務正常狀況下的功能就行了,必定要多想一想,你的代碼在運行的時候,可能會遇到哪些邊界狀況或者異常狀況。遇到了應該如何應對,這樣寫出來的代碼纔夠健壯!

技巧五:舉例畫圖,輔助思考

對於稍微複雜的鏈表操做,好比前面咱們提到的單鏈表反轉,指針一下子指這,一下子指那,一下子就被繞暈了。總感受腦容量不夠,想不清楚。因此這個時候就要使用大招了,舉例法和畫圖法。

技巧六:多寫多練,沒有捷徑

當咱們已經掌握了籤前面所說的方發時,可是手寫鏈表代碼仍是會出現各類各樣的錯誤,也不要着急,咱們寫這些代碼,其實沒什麼技巧,就是把常見的鏈表操做都本身多些幾遍,出問題就一點一點調試,熟能生巧!因此,我精選了 5 個常見的鏈表操做。你只要把這幾個操做都能寫熟練,不熟就多寫幾遍,我保證你以後不再會懼怕寫鏈表代碼。

  1. 單鏈表反轉
  2. 鏈表中環的檢測
  3. 兩個有序的鏈表合併
  4. 刪除鏈表倒數第 n 個結點
  5. 求鏈表的中間結點

鏈表的定義

數據存儲子「節點」(Node)中:

class Node{
    E e;
    Node next;
}

添加元素

1.在鏈表頭部添插入元素

實現:

private void addFirst(E e){
   //建立一個節點
   Node node = new Node();
   //將node的next指向head
   node.next = head;
   //將head更新爲node
   head = node;
   
   size++;
}

2.在索引爲2的地方添加元素666

 

實現:

//在鏈表的中間index(0-based)位置添加元素
    public void add(int index, E data){
        if (index<0 || index>size)
            throw new IllegalArgumentException("Add failed,Illegal index");
        if (index==0)
            addFirst(data);
        else {
            //先找到要插入節點的前一個節點
            Node prev = head;
            for (int i = 0; i < index - 1; i++){//須要知道index的前一個節點,全部遍歷index-1次
               prev = prev.next;             //prev節點向後移動
               Node node = new Node(data);   //先建立一個新節點
               node.next = prev.next;        //先將新節點的next指向prev的next
               prev.next = node;             //將prev的next指向node新節點
//            prev.next = new Node(data,prev.next);
            size++;
        }
    }
}

另外,插入元素很容易將順序顛倒,好比:

若是先執行 prev.next = node (此時prev的next指針已經指向node新節點),接着執行node.next = =prev.next,這時候又將node.next指向他本身(perv.next)明顯邏輯順序戶不合,全部在插入元素的過程,順序很重要。

3.在鏈表的末尾添加元素

實現:

public void addLast(E data){
   add(size,data)
}

刪除元素

數組和鏈表的對比

區別一:物理地址存儲的連續性
數組的元素在內存中是連續存放的。
鏈表的元素在內存中不必定是連續存放的,一般是不連續的。
區別二:訪問速度
數組的訪問速度很快,由於數組能夠根據數組能夠根據下標進行快速定位。
鏈表的訪問速度較慢,由於鏈表訪問元素須要移動指針。
區別三:添加、刪減元素速度
數組的元素增刪速度較慢,由於須要移動大量的元素。
鏈表的元素增刪速度較快,由於只須要修改指針便可。

總結

寫鏈表代碼考驗的是邏輯思惟能力,由於,鏈表代碼處處都是指針的操做、邊界條件的處理,稍有不慎就容易產生 Bug。鏈表代碼寫得好壞,能夠看出一我的寫代碼是否夠細心,考慮問題是否全面,思惟是否縝密。因此,這也是不少面試官喜歡讓人手寫鏈表代碼的緣由。因此,這一節講到的東西,你必定要本身寫代碼實現一下,纔有效果。

相關文章
相關標籤/搜索