資料結構基礎學習之線性表

h_dj發表於2019-04-05

線性表的學習

學習目標

  • 線性表的定義
  • 線性表的儲存方式和表達方式
  • 基本實現
  • 基本操作實現
  • 雙向連結串列插入和刪除實現
  • 迴圈單連結串列和迴圈雙向連結串列的結構特點
1. 線性表:
  • 定義:零個或多個資料元素所構成的有限序列
  • 儲存方式:順序儲存結構和鏈式儲存結構
  • 抽象資料型別描述
public interface IList<E>{
    void clear();//線性表清空操作
    boolean isEmpty();// 判空
    int size(); //長度
    E get(int i);//通過下標獲取元素
    void insert(int i,E t);// 插入元素到特定位置
    void remove(int i);// 移除元素
    int indexOf(E t);// 查詢元素
    void display();//列印元素
}
複製程式碼
2. 線性表順序儲存結構:
  • 定義: 用順序儲存方法儲存的線性表簡稱為順序表(Sequential List)。
  • 節點儲存地址的計算:
    • 假設每個節點佔用c個儲存單元
    • 其中第一個單元的儲存地址則是該結點的儲存地址; 並設表中開始結點a1的儲存地址(簡稱為基地址)是LOC(a1)
    • 所以結點ai的儲存地址LOC(ai): LOC(ai)= LOC(a1)+(i-1)*c 1≤i≤n
  • 順序表的特點
    1. 邏輯上相鄰的結點其物理位置亦相鄰。
    2. 儲存密度高; 儲存密度= 資料元素所需的儲存空間/該資料元素實際所佔空間;需要預先分配"足夠應用"的空間,可能會造成儲存空間浪費
    3. 便於隨機儲存
    4. 不便於隨機插入刪除
3. 順序表基本實現和分析
  1. 刪除操作實現及分析
  • 程式碼實現

    public T remove(int i) {
        // 首先,先判度下標 i 是否合法
        if (i < 0 || i > this.lenght)
            throw new IndexOutOfBoundsException("Index: " + i + ", Size: " + this.lenght);
        //獲取刪除的元素
        T removeObj = (T) objects[i];
        //把下標為i及其後的元素,往前移移一位
        for (int j = i; j < this.lenght - 1; j++) {
            objects[j] = objects[j + 1];
        }
        //把最後一個元素置空,幫助垃圾回收
        objects[lenght - 1] = null;
        //當前線性表長度減一
        --lenght;
        return removeObj;
    }
複製程式碼
  • 時間複雜度分析
    • 在n個元素的順序表中,刪除第i各元素,則0<=i<=n-1
    • 假設刪除的概率相同,則p = 1/n
    • 刪除第i個元素後,需要移動 n-i-1個元素
    • 平均移動的次數為 (1/n) * (n-i-1)求和
    • 所以時間複雜度為:O(n)
  1. 插入操作及分析
  • 程式碼實現
 public void insert(int i, T t) {
        // 首先,先判度下標 i 是否合法
        if (i < 0 || i > this.lenght)
            throw new IndexOutOfBoundsException("Index: " + i + ", Size: " + this.lenght);

        //判斷是否超出順序表的容量
        if (this.lenght >= this.objects.length)
            throw new ArrayIndexOutOfBoundsException("length = " + this.lenght + " Capacity" + this.initCapacity);

        //把下標為i及其後的元素,往後移移一位
        for (int j = this.lenght - 1; j >= i; --j) {
            objects[j + 1] = objects[j];
        }
        //插入元素
        objects[i] = t;
        lenght++;
    }
複製程式碼
  • 時間複雜度分析
    • 在長度為 n 的順序表中,在第i個位置插入一個元素, 0<= i <= n
    • 插入元素前需要將第i個元素開始往後移動,需要移動n-i個元素
    • 假設插入每個位置的概率相同,則p = 1/(n+1)
    • 所以,平均移動次數為 (1/(n+1)) * (n-i) 求和 (0<= i <= n)
    • 則,平均時間複雜度為: O(n)
  1. 查詢元素操作及其分析
  • 程式碼實現
public int indexOf(E e) {
    //先判斷 元素是否為空,可防止空指標的出現
    if (t == null) {
        //為空則,返回順序表中空元素的下標
        for (int i = 0; i < this.lenght; i++)
            if (objects[i] == null)
                return i;
    } else {
        //不為空,則返回與之匹配的元素下標
        for (int i = 0; i < this.lenght; i++)
            if (t.equals(objects[i]))
                return i;
    }
    return -1;
    }
複製程式碼
  • 時間複復雜度分析
    • 假設在n個元素的順序表中,第i個元素為查詢的元素x
    • 那麼,比較的次數為i+1次,如果沒有找到,則需要比較n次
    • 假設查詢每個元素的概率相同,p= (1/n)
    • 所以,平均比較次數為 (1/n)*(i+1)求和 (0<=i<=n-1)
    • 時間複雜度為:O(n)
  1. SqList 完整原始碼
4. 連結串列的概念
  1. 定義:
  •  連結方式儲存的線性表簡稱為連結串列(Linked List)。
  1. 儲存結構定義:
  • 用一組任意的儲存單元來存放線性表的結點
  • 連結串列中結點的邏輯次序和物理次序不一定相同
  • 每個結點由:資料域(存放資料資訊)和指標域(存放直接後繼節點地址)兩部分組成
  • 注意:
    • 鏈式儲存是最常用的儲存方式之一
    • 它不僅可用來表示線性表,而且可用來表示各種非線性的資料結構
  1. 連結串列的結點結構

2.9 節點類的結構圖.png

  • data域--存放結點值的資料域
  • next域--存放結點的直接後繼的地址(位置)的指標域(鏈域)
  • 注意:
    • 連結串列通過每個結點的鏈域將線性表的n個結點按其邏輯順序連結在一起的。
    • 每個結點只有一個鏈域的連結串列稱為單連結串列(Single Linked List)。
  1. 單連結串列的表示

2.7 單連結串列儲存示意圖.png

  • 頭節點和頭指標的區別 1. 連結串列中的第一個節點的儲存位置叫做頭指標 2. 連結串列中的第一個節點前預設的一個節點叫做頭節點 3. 頭指標是連結串列必要元素 4. 頭節點不一定是連結串列的必要元素

2.8 帶頭結點單連結串列儲存示意圖.png

5. 連結串列的實現及分析
  1. 結點類
public class LNode<T> {
    //資料域
    public T data;
    //指標域
    public LNode<T> next;
    //...略
}
複製程式碼
  1. 查詢操作
  • 程式碼實現
//按序號查詢
 public T get(int i) {
        //獲取第一個節點元素
        LNode node = head.next;
        //計數器
        int pos = 0;
        //遍歷節點,直到節點為空 或者 指向第 i 個節點退出迴圈
        while (node != null && pos < i) {
            node = node.next; //指向後繼節點
            ++pos;//計數器加一
        }
        //判斷是否找到節點
        if (node == null || pos > i)
            throw new RuntimeException("第 " + i + " 個元素不存在!");

        return (T) node.data;
    }
  
  
  //按元素值查詢
   public int indexOf(T t) {
          //獲取第一個節點元素
          LNode node = head.next;
          //計數器
          int pos = 0;
  
          //判斷查詢的值是否為空
          if (t == null) {
              //遍歷節點,直到節點為空 或者 節點的資料域為空,退出迴圈
              while (node != null) {
                  if (node.data == null) {
                      return pos;
                  }
                  node = node.next; //指向後繼節點
                  ++pos;//計數器加一
              }
          } else {
              //遍歷節點,直到節點為空 或者 指向值為 t的 節點退出迴圈
              while (node != null) {
                  if (t.equals(node.data)) {
                      return pos;
                  }
                  node = node.next; //指向後繼節點
                  ++pos;//計數器加一
              }
          }
          return -1;
      }
複製程式碼
  • 時間複雜度:每次查詢都是表頭開始遍歷查詢,所以時間複雜度為:O(n)
  1. 插入操作

2.10 單連結串列插入操作.png

  • 程式碼實現
//帶頭結點連結串列插入操作
    public void insert(int i, T t) {
            LNode p = head;
            int pos = -1;
            //1. 找到第 (i-1)個節點(位置 i 的前驅節點)
            while (p.next != null && pos < i - 1) {
                p = p.next;
                pos++;
            }
            //判斷插入位置的合法性
            if (p == null || pos > i - 1)
                throw new RuntimeException("插入節點的位置不合法!");
    
            //2. 建立一個新的節點
            LNode newNode = new LNode(t);
            //3.1 新節點的後繼指標指向 原先第 i個節點
            newNode.next = p.next;
            //3.2  第(i-1)節點 p 的後繼指標指向新節點
            p.next = newNode;
        }
        
    
    //不帶頭結點
    public void insert(int i, T t) {
        LNode p = head;
        int pos = 0;
    
        while (p.next != null && pos < i - 1) {
            p = p.next;
            pos++;
        }
        //判斷插入位置的合法性
        if (p == null || pos > i)
            throw new RuntimeException("插入節點的位置不合法!");

        //2. 建立一個新的節點
        LNode newNode = new LNode(t);
        if(i==0){
        //插入表頭時
            newNode.next = head;
            head = newNode;
        }else{
             newNode.next = p.next;
              p.next = newNode;
        }
    }
複製程式碼
  • 時間複雜度:在第i個元素插入結點,需要找到第(i-1)個結點,時間複雜度:O(n)
  1. 刪除操作

2.12單連結串列刪除操作.png

  • 程式碼實現
 public T remove(int i) {
        LNode p = head;
        int pos = -1;
        //找到待刪除節點的前驅節點
        while (p.next != null && pos < i - 1) {
            p = p.next;
            ++pos;
        }
        if (pos > i - 1 || p == null)
            throw new RuntimeException("刪除節點的位置不合法!");
        //待刪除節點
        LNode remove = p.next;
        //3. 第 (i-1) 節點的指標指向 (i+1)節點
        p.next = remove.next;
        return (T) remove.data;
    }
複製程式碼
  • 時間複雜度:O(n)
  1. 單連結串列的建立
  • 示意圖

2.13頭部插入法示意圖.png

  • 頭插入法
 public void insertAtHead(T t) {
        //構建新插入的節點
        LNode newNode = new LNode(t);
        //新節點的後繼指標指向頭結點的頭指標
        newNode.next = head.next;
        //頭指標指向新節點
        head.next = newNode;
    }
複製程式碼

2.14 尾部插入法示意圖.png

  • 尾插法
public void insertTail(T t) {
    //獲取到最後的節點
    LNode tail = this.head;
    while (tail.next != null) {
        tail = tail.next;
    }
    //構造新的節點
    LNode newNode = new LNode(t);
    //新節點指標指向 尾節點指標
    newNode.next = tail.next;
    //尾節點指標指向新節點
    tail.next = newNode;
}
複製程式碼
  1. 單連結串列實現原始碼
6. 迴圈連結串列

2.18帶頭結點的迴圈連結串列儲存示意圖.png

  1. 實現迴圈連結串列的方式
  • 使用頭指標的方式
  • 使用尾指標的方式
  • 使用頭尾指標的方法
  1. 迴圈連結串列尾指標方式實現原始碼
7. 雙向連結串列

2.20帶頭結點的雙向連結串列.png

  1. 結點類
public class DuLNode<E> {
    public E data;//資料域
    public DuLNode<E> prior;//前驅指標
    public DuLNode<E> next;//後驅指標
    public DuLNode() {
        this(null);
    }
    public DuLNode(E data) {
        this.data = data;
        this.prior = null;
        this.next = null;
    }
}
複製程式碼
  1. 插入操作
    2.22雙向連結串列插入.png
    public void insert(int i, E t) {
        //先判斷索引是否合法
        if (i < 0 || i > length)
            throw new RuntimeException("插入元素的位置不合法! i=" + i);
        DuLNode<E> p = head;
        //下標
        int index = -1;
        //找到第i個元素
        while (p.next != null && index < i) {
            index++;
            p = p.next;
        }
        //建立一個新的結點
        DuLNode<E> newNode = new DuLNode<>(t);
        if (i == 0 || i == length) {
            newNode.prior = p;
            p.next = newNode;
        } else {
            //1. 第i個結點p的前驅結點的後繼指向新結點
            p.prior.next = newNode;
            //2. 新結點的前驅指向第(i-1)個結點
            newNode.prior = p.prior;
            //3. 新結點的後驅指向第i個結點p
            newNode.next = p;
            //4. 第i個結點p的前驅指向新結點
            p.prior = newNode;
        }
        //長度加一
        length++;
    }
複製程式碼
  1. 刪除操作
    2.23雙向連結串列刪除.png
    public E remove(int i) {

        //先判斷索引是否合法
        if (i < 0 || i > length - 1)
            throw new RuntimeException("刪除元素不存在! i=" + i);

        DuLNode<E> p = head;
        //下標
        int index = -1;
        //找到第i個元素
        while (p.next != null && index < i) {
            index++;
            p = p.next;
        }

        DuLNode<E> remove = p;

        //1. 第(i-1)個結點的後驅指向 第 (i+1)個結點
        p.prior.next = p.next;
        //2. 第 (i+1)個結點的前驅指向 第(i-1)個結點
        p.next.prior = p.prior;

        //長度減一
        length--;
        return remove.data;
    }
複製程式碼
  1. 雙向連結串列完整原始碼
8. 連結串列與順序表的比較
  • 基於空間考慮
    • 分配方式
      • 順序表:靜態分配。
      • 連結串列:動態分配。
      • 難以估計其儲存規模時,以採用動態連結串列作為儲存結構為好。
    • 儲存密度
      • 順序表:=1
      • 連結串列:<1
      • 為了節約儲存空間,宜採用順序表作為儲存結構。
      • 儲存密度=(結點資料本身所佔的儲存量)/(結點結構所佔的儲存總量)
  • 基於時間考慮
    • 存取方法
      • 順序表: 隨機儲存結構,時間複雜度O(1);
      • 連結串列:順序存取結構,時間複雜度O(n)
      • 操作主要是進行查詢,很少做插入和刪除操作時,採用順序表做儲存結構為宜
    • 插入刪除操作
      • 在順序表中進行插入和刪除,平均要移動表中近一半的結點,尤其是當每個結點的資訊量較大時,移動結點的時間開銷就相當可觀。
      • 在連結串列中的任何位置上進行插入和刪除,都只需要修改指標。

相關文章