數據結構與算法之線性表(超詳細順序表、鏈表)

前言

經過前面數據結構與算法基礎知識我麼知道了數據結構的一些概念和重要性,那麼咱們今天總結下線性表相關的內容。固然,我用本身的理解解分享給你們。java

其實說實話,可能不少人依然分不清線性表順序表,和鏈表之間的區別和聯繫!node

  • 線性表:邏輯結構, 就是對外暴露數據之間的關係,不關心底層如何實現,數據結構的邏輯結構大分類就是線性結構和非線性結構而順序表、鏈表都是一種線性表。
  • 順序表、鏈表:物理結構,他是實現一個結構實際物理地址上的結構。好比順序表就是用數組實現。而鏈表用指針完成主要工做。不一樣的結構在不一樣的場景有不一樣的區別。

在Java中,你們都知道List接口類型,這就是邏輯結構,由於他就是封裝了一個線性關係的一系列方法和數據。而具體的實現其實就是跟物理結構相關的內容。好比順序表的內容存儲使用數組的,而後一個get,set,add方法都要基於數組來完成,而鏈表是基於指針的。當咱們考慮對象中的數據關係就要考慮指針的屬性。指針的指向和value。c++

下面用一個圖來淺析線性表的關係。可能有些不太確切,可是其中能夠參考,而且後面也會根據這個圖舉例。
image-20210104160901005git

線性表基本架構

對於一個線性表來講。無論它的具體實現如何,可是它們的方法函數名和實現效果應該一致(即便用方法相同、達成邏輯上效果相同,差異的是運行效率)。線性表的概念與Java的接口/抽象類有那麼幾分類似。最著名的就是List的Arraylist和LinkedList,List是一種邏輯上的結構,表示這種結構爲線性表,而ArrayList,LinkedList更多的是一種物理結構(數組和鏈表)。github

因此基於面向對象的編程思惟,咱們能夠將線性表寫成一個接口,而具體實現的順序表和鏈表的類能夠實現這個線性表的方法,提升程序的可讀性,還有一點比較重要的,記得初學數據結構與算法時候實現的線性表都是固定類型(int),隨着知識的進步,咱們應當採用泛型來實現更合理。至於接口的具體設計以下:算法

package LinerList;
public interface ListInterface<T> {    
    void Init(int initsize);//初始化表
    int length();
    boolean isEmpty();//是否爲空
    int ElemIndex(T t);//找到編號
    T getElem(int index) throws Exception;//根據index獲取數據
    void add(int index,T t) throws Exception;//根據index插入數據
    void delete(int index) throws Exception;
    void add(T t) throws Exception;//尾部插入
    void set(int index,T t) throws Exception;
    String toString();//轉成String輸出    
}

順序表

順序表是基於數組實現的,因此全部實現須要基於數組特性。對於順序表的結構應該有一個存儲數據的數組data和有效使用長度length.編程

還有須要注意的是初始化數組的大小,你能夠固定大小,可是筆者爲了可用性若是內存不夠將擴大二倍。數組

下面着重講解一些初學者容易混淆的概念和方法實現。這裏把順序表比做一隊坐在板凳上的人。數據結構

插入操做

add(int index,T t) 架構

其中index爲插入的編號位置,t爲插入的數據,插入的流程爲:

(1)從後(最後一個有數據位)向前到index依次後移一位,騰出index位置的空間

(2)將待插入數據賦值到index位置上,完成插入操做

image-20210104194146220

能夠看得出若是順序表很長,在靠前的地方若是插入效率比較低(插入時間複雜度爲O(n)),若是頻繁的插入那麼複雜度挺高的。

刪除操做

同理,刪除也是很是佔用資源的。原理和插入相似,刪除index位置的操做就是從index+1開始向後依次將數據賦值到前面位置上,具體能夠看這張圖:

image-20210104201346674

代碼實現

這裏我實現一個順序表給你們做爲參考學習:

package LinerList;

public class seqlist<T> implements ListInterface<T> {
    private Object[] date;//數組存放數據
    private int lenth;
    public seqlist() {//初始大小默認爲10
        Init(10);
    }

    public void Init(int initsize) {//初始化
        this.date=new Object[initsize];
        lenth=0;        
    }
    public int length() {        
        return this.lenth;
    }

    public boolean isEmpty() {//是否爲空
        if(this.lenth==0)
            return true;
        return false;
    }

    /*
     * * @param t    
     * 返回相等結果,爲-1爲false
     */
    public int ElemIndex(T t) {
        // TODO Auto-generated method stub
        for(int i=0;i<date.length;i++)
        {
            if(date[i].equals(t))
            {
                return i;
            }
        }
        return -1;
    }

    /*
     *得到第幾個元素
     */
    public T getElem(int index) throws Exception {
        // TODO Auto-generated method stub
        if(index<0||index>lenth-1)
            throw new Exception("數值越界");
        return (T) date[index];
    }
    
    public void add(T t) throws Exception {//尾部插入
         add(lenth,t);
    }

    /*
     *根據編號插入
     */
    public void add(int index, T t) throws Exception {
        if(index<0||index>lenth)
            throw new Exception("數值越界");
        if (lenth==date.length)//擴容
        {
            Object newdate[]= new Object[lenth*2];
            for(int i=0;i<lenth;i++)
            {
                newdate[i]=date[i];
            }
            date=newdate;
        }
        for(int i=lenth-1;i>=index;i--)//後面元素後移動
        {
            date[i+1]=date[i];
        }
        date[index]=t;//插入元素
        lenth++;//順序表長度+1
        
    }

    public void delete(int index) throws Exception {
        if(index<0||index>lenth-1)
            throw new Exception("數值越界");
        for(int i=index;i<lenth;i++)//index以後元素前移動
        {
            date[i]=date[i+1];
        }
        lenth--;//長度-1    
    }

    @Override
    public void set(int index, T t) throws Exception {
        if(index<0||index>lenth-1)
            throw new Exception("數值越界");
        date[index]=t;
    }
    public String  toString() {
        String vaString="";
        for(int i=0;i<lenth;i++)
        {
            vaString+=date[i].toString()+" ";
        }
        return vaString;
        
    }
}

鏈表

學習c/c++的時候鏈表應該是不少人感受很繞的東西,這個很大緣由可能由於指針,Java雖然不直接使用指針可是咱們也要理解指針的原理和運用。鏈表不一樣於順序表(數組)它的結構像一條鏈同樣連接成一個線性結構,而鏈表中每個節點都存在不一樣的地址中,鏈表你能夠理解爲它存儲了指向節點(區域)的地址,可以經過這個指針找到對應節點。

對於物理存儲結構,地址之間的聯繫是沒法更改的,相鄰就是相鄰。但對於鏈式存儲,下一位的地址是上一個主動記錄的,能夠進行更改。這就比如親兄弟從出生就是同姓兄弟,而咱們在成長途中最好的朋友可能會因爲階段性發生一些變化!

就如西天取經的唐僧、悟空、八戒、沙和尚。他們本無聯繫,但結拜爲師徒兄弟,你問悟空他的師父它會立馬想到唐僧,由於五指山下的約定。

image-20210104215538219

基本結構

對於線性表,咱們只須要一個data數組和length就能表示基本信息。而對於鏈表,咱們須要一個node(head頭節點),和length分別表示存儲的節點數據和鏈表長度,這個節點有數據域指針域。數據域就是存放真實的數據,而指針域就是存放下一個node的指針,其具體結構爲:

class node<T>{
    T data;//節點的結果
    node next;//下一個鏈接的節點
    public node(){}
    public node(T data)
    {
        this.data=data;
    }
    public node(T data, node next) {
        this.data = data;
        this.next = next;
    } 
}

帶頭結點鏈表VS不帶頭結點鏈表

有不少人會不清楚帶頭結點和不帶頭結點鏈表的區別,甚至搞不懂什麼是帶頭結點和不帶頭結點,我給你們闡述一下:

帶頭結點:head指針始終指向一個節點,這個節點不存儲有效值僅僅起到一個標識做用(至關於班主任帶學生)

不帶頭結點:head指針始終指向第一個有效節點,這個節點儲存有效數值。

那麼帶頭結點和不帶頭結點的鏈表有啥區別呢?

查找上:無大區別,帶頭結點須要多找一次。

插入上:非第0個位置插入區別不大,不帶頭結點的插入第0號位置以後須要從新改變head頭的指向。

image-20210104231252005

刪除上:非第0個位置刪除區別不大,不帶頭結點的刪除第0號位置以後須要從新改變head頭的指向。

頭部刪除(帶頭節點):帶頭節點的刪除和普通刪除同樣。直接head.next=head.next.next,這樣head.next就直接指向第二個元素了。第一個就被刪除了

頭部刪除(不帶頭節點):不帶頭節點的第一個節點(head)就存儲有效數據。不帶頭節點刪除也很簡單,直接將head指向鏈表中第二個node節點就好了。即:head=head.next

image-20210104231732318

總而言之:帶頭結點經過一個固定的頭可使鏈表中任意一個節點都同等的插入、刪除。而不帶頭結點的鏈表在插入、刪除第0號位置時候須要特殊處理,最後還要改變head指向。二者區別就是插入刪除首位(尤爲插入)固然我是建議你之後在使用鏈表時候儘可能用帶頭結點的鏈表避免沒必要要的麻煩。

帶頭指針VS帶尾指針

基本上是個鏈表都是要有頭指針的,那麼頭尾指針是個啥呢?

頭指針: 其實頭指針就是鏈表中head節點,成爲頭指針。

尾指針: 尾指針就是多一個tail節點的鏈表,尾指針的好處就是進行尾插入的時候能夠直接插在尾指針的後面,而後再改變一下尾指針的順序便可。

image-20210105111135281

可是帶尾指針的單鏈表若是刪除尾的話效率不高,須要枚舉整個鏈表找到tail前面的那個節點進行刪除。

插入操做

add(int index,T t)
其中index爲插入的編號位置,t爲插入的數據,在帶頭結點的鏈表中插入在任何位置都是等效的。
加入插入一個節點node,根據index找到插入的前一個節點叫pre。那麼操做流程爲

1. `node.next=pre.next`,將插入節點後面先與鏈表對應部分聯繫起來。此時node.next和pre.next一致。
2. `pre.next=node` 將node節點插入到鏈表中。

image-20210105001736232

固然,不少時候鏈表須要插入在尾部,若是頻繁的插入在尾部每次枚舉到尾部的話效率可能比較低,可能會藉助一個尾指針去實現尾部插入。

刪除操做

按照index移除(主要掌握):delete(int index)

本方法爲帶頭結點普通鏈表的通用方法(刪除尾也同樣),找到該index的前一個節點pre,pre.next=pre.next.next

image-20210105161038897

代碼實現

在這裏我也實現一個單鏈表給你們做爲參考使用:

package LinerList;

class node<T>{
    T data;//節點的結果
    node next;//下一個鏈接的節點
    public node(){}
    public node(T data)
    {
        this.data=data;
    }
    public node(T data, node next) {
        this.data = data;
        this.next = next;
    }
   
}
public class Linkedlist<T> implements ListInterface<T>{

    node head;
    private int length;
    public Linkedlist() {
        head=new node();
        length=0;
    }
    public void Init(int initsize) {
        head.next=null;
        
    }

    public int length() {
        return this.length;
    }

    
    public boolean isEmpty() {
        if(length==0)return true;
        else return false;
    }

    /*
     * 獲取元素編號
     */
    public int ElemIndex(T t) {
        node team=head.next;
        int index=0;
        while(team.next!=null)
        {
            if(team.data.equals(t))
            {
                return index;
            }
            index++;
            team=team.next;
        }
        return -1;//若是找不到
    }

    @Override
    public T getElem(int index) throws Exception {
        node team=head.next;
        if(index<0||index>length-1)
        {
            throw new Exception("數值越界");
        }
        for(int i=0;i<index;i++)
        {
            team=team.next;
        }
        return (T) team.data;
    }


    public void add(T t) throws Exception {
        add(length,t);
        
    }
    //帶頭節點的插入,第一個和最後一個同樣操做
    public void add(int index, T value) throws Exception {
        if(index<0||index>length)
        {
            throw new Exception("數值越界");
        }
        node<T> team=head;//team 找到當前位置node
        for(int i=0;i<index;i++)
        {
             team=team.next;
        }
        node<T>node =new node(value);//新建一個node
        node.next=team.next;//指向index前位置的下一個指針
        team.next=node;//本身變成index位置    
        length++;
    }
    

    @Override
    public void delete(int index) throws Exception {
        if(index<0||index>length-1)
        {
            throw new Exception("數值越界");
        }
        node<T> team=head;//team 找到當前位置node
        for(int i=0;i<index;i++)//標記team 前一個節點
        {
             team=team.next;
        }
        //team.next節點就是咱們要刪除的節點
        team.next=team.next.next;
        length--;
    }

    @Override
    public void set(int index, T t) throws Exception {
        // TODO Auto-generated method stub
        if(index<0||index>length-1)
        {
            throw new Exception("數值越界");
        }
        node<T> team=head;//team 找到當前位置node
        for(int i=0;i<index;i++)
        {
             team=team.next;
        }
        team.data=t;//將數值賦值,其餘不變
        
    }

    public String toString() {
        String va="";
        node team=head.next;
        while(team!=null)
        {
            va+=team.data+" ";
            team=team.next;
        }
        return va;
    }

}

總結

你可能會問這個是否正確啊,那我來測試一下:

image-20210105164129930

這裏的只是簡單實現,實現基本方法。鏈表也只是單鏈表。完善程度還能夠優化。能力有限, 若是有錯誤或者優化的地方還請大佬指正

單鏈表查詢速度較慢,由於他須要從頭遍歷,若是在尾部插入,能夠考慮設計帶尾指針的鏈表。而順序表查詢速度雖然快可是插入很費時費力,實際應用根據需求選擇

Java中的Arraylist和LinkedList就是兩種方式的表明,不過LinkedList使用雙向鏈表優化,而且JDK也作了大量優化。因此你們不用造輪子,能夠直接用,可是手寫順序表、單鏈表仍是頗有學習價值的。

單鏈表搞懂了,雙鏈表也就不遠了(下集預告)!

原創公衆號: bigsai
文章已收錄在 全網都在關注的數據結構與算法學習倉庫 歡迎star
相關文章
相關標籤/搜索