數據結構與算法(二),線性表

轉載請註明出處:http://www.cnblogs.com/wangyingli/p/5928258.htmlhtml

上一篇《數據結構與算法(一),概述》中介紹了數據結構的一些基本概念,並分別舉例說明了算法的時間複雜度和空間複雜度的求解方法。這一篇主要介紹線性表。java

本節內容:node

  • 1、基本概念
  • 2、順序表
  • 3、鏈表
    • 一、單向鏈表
    • 二、單向循環鏈表
    • 三、雙向鏈表
    • 四、靜態鏈表

1、基本概念

線性表是具備零個或多個數據元素的有限序列。線性表中數據元素之間的關係是一對一的關係,即除了第一個和最後一個數據元素以外,其它數據元素都是首尾相接的。面試

線性表的基本特徵:算法

  • 第一個數據元素沒有前驅元素;
  • 最後一個數據元素沒有後繼元素;
  • 其他每一個數據元素只有一個前驅元素和一個後繼元素。

抽象數據類型:數組

線性表通常包括插入、刪除、查找等基本操做。其基於泛型的API接口代碼以下:數據結構

public interface List<E> {
    //線性表的大小
    int size();

    //判斷線性表是否爲空
    boolean isEmpty();

    void clear();

    //添加新元素
    void add(E element);

    //在指定位置添加新元素
    void add(int index, E element);

    //刪除元素
    E delete(int index);

    //獲取元素
    E get(int index);
}

線性表按物理存儲結構的不一樣可分爲順序表(順序存儲)和鏈表(鏈式存儲):ide

  • 順序表(存儲結構連續,數組實現)
  • 鏈表(存儲結構上不連續,邏輯上連續)

2、順序表

順序表是在計算機內存中以數組的形式保存的線性表,是指用一組地址連續的存儲單元依次存儲數據元素的線性結構。線性表採用順序存儲的方式存儲就稱之爲順序表。函數

其插入刪除操做如圖所示:性能

注意:

  • 插入操做:移動元素時,要從後往前操做,不能從前日後操做,否則元素會被覆蓋。
  • 刪除操做:移動元素時,要從前日後操做。

代碼以下:

import java.util.*;
public class SequenceList<E> implements List<E>, Iterable<E> {

    private static final int DEFAULT_CAPACITY = 10;
    private int size;
    private E[] elements;

    @SuppressWarnings("unchecked")
    public SequenceList() {
        size = 0;
        elements = (E[])new Object[DEFAULT_CAPACITY];
    }

    public int size() { return size;}

    public boolean isEmpty(){ return size == 0;}

    @SuppressWarnings("unchecked")
    public void clear(){
        size = 0;
        elements = (E[])new Object[DEFAULT_CAPACITY];
    }

    public void add(E element){ 
        add(size, element);
    }

    //在index插入element
    public void add(int index, E element){
        if(size >= elements.length) {
            throw new RuntimeException("順序表已滿,沒法添加"); 
        }
        if(index < 0 || index > size) {
            throw new IndexOutOfBoundsException("參數輸入錯誤"); 
        }
        for(int i=size; i>index; i--) {
            elements[i] = elements[i - 1];
        }
        elements[index] = element;
        size++;
    }

    //刪除元素
    public E delete(int index){
        if(isEmpty()) {
            throw new RuntimeException("順序表爲空,沒法刪除"); 
        }
        if(index < 0 || index >= size) {
            throw new IndexOutOfBoundsException("參數輸入錯誤"); 
        }
        E result = elements[index];
        for(int i=index; i<size - 1; i++) {
            elements[i] = elements[i + 1];
        }
        size--;
        elements[size] = null; //避免對象遊離
        return result;
    }

    public E get(int index){
        if(index < 0 || index >= size) {
            throw new IndexOutOfBoundsException("參數輸入錯誤"); 
        }
        return elements[index];
    }

    @Override
    public Iterator<E> iterator() {
        return new Iterator<E>() {
            int num = 0;
            @Override
            public E next() {                
                return elements[num++];
            }

            @Override
            public boolean hasNext() {
                return num < size;
            }
        };
    }

    public static void main(String[] args) {
        SequenceList<Integer> sl = new SequenceList<Integer>();
        for(int i=0;i<10;i++) {
            sl.add(i);
        }
        System.out.println("刪除1位置元素:"+sl.delete(1));
         sl.add(0,15);
        for(int i=0;i<sl.size();i++) {
            System.out.print(sl.get(i)+" ");
        }
    }
}

這裏須要注意,因爲java中不能直接建立泛型數組,因此在順序表的構造函數中先建立了一個Object的數組,而後將它強轉爲泛型數組並使用@SuppressWarnings("unchecked")消除未受檢的警告。若對這點還有什麼疑問能夠參考個人學習筆記 Effective java筆記(四),泛型 中第2五、26條。另外在進行刪除操做時應避免對象遊離。

在java中,數組一旦建立其大小不能改變,因此在上面的實現中,爲了儘量的不浪費內存必須事先準確的預估順序表的容量。但現實應用中因爲存在不少不肯定因素,這每每是不切實際的。這時可以使用動態調整數組大小的方法來解決這個問題。代碼以下:

private void resize(int num){
    @SuppressWarnings("unchecked")
    E[] temp = (E[]) new Object[num];
    for(int i=0; i<size; i++) {
        temp[i] = elements[i];
    }
    elements = temp;
}

而後在插入和刪除操做中分別加入判斷語句,來調用這個方法

//在index插入element
public void add(int index, E element){
    //當順序表滿時,容量加倍
    if(size >= elements.length) {
        // throw new RuntimeException("順序表已滿,沒法添加"); 
        resize(elements.length*2);
    }
    if(index < 0 || index > size) {
        throw new IndexOutOfBoundsException("參數輸入錯誤"); 
    }
    ....
}

//刪除元素
public E delete(int index){
    ....
    elements[size] = null;

    //當元素數量小於容量的1/4時,容量減半
    if(size>0 && size <= elements.length/4) {
        resize(elements.length/2);
    }
    return result;
}

注意:在刪除操做中檢查條件爲「順序表的大小是否小於容量的 1/4」,而不是1/2。這樣能夠避免在1/2這個零界點處反覆進行插入刪除操做時,數組進行頻繁複制。

順序表效率分析:

  • 順序表插入和刪除一個元素,最好狀況下其時間複雜度(這個元素在最後一個位置)爲O(1),最壞狀況下其時間複雜度爲O(n)。
  • 順序表支持隨機訪問,讀取一個元素的時間複雜度爲O(1)。

順序表的優缺點:

  • 優勢:支持隨機訪問
  • 缺點:插入和刪除操做須要移動大量的元素,形成存儲空間的碎片。

順序表適合元素個數變化不大,且更可能是讀取數據的場合。

3、鏈表

鏈表是一種物理存儲單元上非連續、非順序的存儲結構,數據元素的邏輯順序是經過鏈表中的指針連接次序實現的。鏈表由一系列結點組成,每一個結點包括兩個部分:一個是存儲數據元素的數據域,另外一個是存儲下一個結點地址的指針域。

鏈表根據構造方式的不一樣能夠分爲:

  • 單向鏈表
  • 單向循環鏈表
  • 雙向鏈表

一、單向鏈表

單鏈表有帶頭結點和不帶頭結點兩種結構,其結構以下

在帶頭結點的單鏈表中,其第一個結點被稱做頭結點。第一個存放數據元素的結點稱做首元結點,頭結點指向首元結點。頭結點是爲了操做的統一與方便而設立的,其通常不放數據(也可存放鏈表的長度、用作監視哨等)。此結點不能計入鏈表長度值

帶頭結點的單鏈表的優勢:

  • 在鏈表第一個位置上進行的操做(插入、刪除)和其它位置上的操做一致,無須進行特殊處理;
  • 不管鏈表是否爲空,head必定不爲空,這使得空表和非空表的處理一致。

因爲帶頭結點的鏈表更容易操做,這裏僅實現帶頭結點的單鏈表

帶頭結點的鏈表插入與刪除示意圖:

代碼以下:

import java.util.*;
public class LinkedList<E> implements List<E>, Iterable<E>{
    private Node head;
    private int size;

    private class Node {
        E element;
        Node next;
    }

    LinkedList() {
        head = new Node();
    }

    @Override public int size() { return size;}
    
    @Override public boolean isEmpty() { return size==0;}

    @Override public void clear() {
        head = new Node();
        size = 0;
    }

    @Override public void add(E element) {
        add(0, element);
    }

    @Override public void add(int index, E element) {
        if(index < 0 || index > size)
            throw new IndexOutOfBoundsException("參數輸入錯誤");
        Node current = location(index);
        Node newNode = new Node();
        newNode.element = element;        
        Node node = current.next;
        current.next = newNode;
        newNode.next = node;
        size++;
    }

    //找到第index個結點前的結點
    private Node location(int index){
        Node current = head;
        for(int i=0; i<index; i++) {
            current = current.next;
        }
        return current;
    }
    
    @Override public E get(int index) {
        if(index < 0 || index >= size)
            throw new IndexOutOfBoundsException("參數輸入錯誤");
        return location(index + 1).element;
    }

    //刪除第index個元素
    @Override public E delete(int index) {
        if(index < 0 || index >= size)
            throw new IndexOutOfBoundsException("參數輸入錯誤");
        Node current = location(index);
        E element = current.next.element;
        current.next = current.next.next;
        size--;
        return element;
    }

    @Override
    public Iterator<E> iterator() {
        return new Iterator<E>() {
            Node current = head;
            @Override
            public E next() {  
                current = current.next;              
                return current.element;
            }

            @Override
            public boolean hasNext() {
                return current.next != null;
            }
        };
    }

    public static void main(String[] args) throws Exception{
        LinkedList<Integer> list = new LinkedList<Integer>();
        for(int i=0;i<10;i++) {
            list.add(i);
        }
        System.out.println("刪除0位置元素:"+list.delete(0));
        list.add(0,15);

        for (Integer ele : list ) {
            System.out.print(ele + " ");
        }
    }
}

單鏈表效率分析:

在單鏈表上插入和刪除數據時,首先須要找出插入或刪除元素的位置。對於單鏈表其查找操做的時間複雜度爲 O(n),因此

  • 鏈表插入和刪除操做的時間複雜度均爲 O(n)

  • 鏈表讀取操做的時間複雜度爲 O(n)

單鏈表優缺點:

  • 優勢:不須要預先給出數據元素的最大個數,單鏈表插入和刪除操做不須要移動數據元素
  • 缺點:不支持隨機讀取,讀取操做的時間複雜度爲 \(O(n)\)

二、單向循環鏈表

將單鏈表中終端結點的指針指向頭結點,使整個單鏈表造成一個環,這種頭尾相接的單鏈表稱爲單循環鏈表,簡稱循環鏈表。

對於循環鏈表,爲了使空鏈表與非空鏈表處理一致,一般設一個頭結點。以下圖:

循環鏈表和單鏈表的主要差別在於鏈表結束的判斷條件不一樣,單鏈表爲current.next是否爲空,而循環鏈表爲current.next不等於頭結點。對於循環鏈表的增刪改查操做與單鏈表基本相同,僅僅須要將鏈表結束的條件變成current.next != head便可,這裏就不在給出了。

在單鏈表中,咱們有了頭結點時,對於最後一個結點的訪問須要 O(n)的時間,由於咱們須要將單鏈表所有遍歷一次。哪有沒有可能用 O(1)的時間訪問到終端結點呢?固然能夠,咱們只需改造一下單鏈表,使用指向終端結點的尾指針來表示循環鏈表,這時訪問開始結點(不是頭結點)和終端結點的操做都爲 O(1)。它們的訪問操做分別爲end.next.nextend,其中end爲指向終端結點的引用。這個設計對兩個循環鏈表的合併特別有用,能夠避免遍歷鏈表的時間消耗。如:

合併兩個循環鏈表的代碼:

public Node merge(Node endA, Node endB) {
    Node headA = endA.next; //保存A表的頭結點
    endA.next = endB.next.next;
    endB.next = headA;
    return endB;
}

三、雙向鏈表

雙向鏈表是在單鏈表的每一個結點中,再設置一個指向其前驅結點的指針域。使得兩個指針域一個指向其前驅結點,一個指向其後繼結點。

雙向鏈表的結點表示:

private class Node {
    E element;
    Node prior; //指向前驅
    Node next;
}

對於雙向鏈表,其空和非空結構以下圖:

雙向鏈表是單鏈表擴展出來的結構,它能夠反向遍歷、查找元素,它的不少操做和單鏈表相同,好比求長度size()、查找元素get()。這些操做只涉及一個方向的指針便可。插入和刪除操做時,須要更改兩個指針變量。

插入操做:注意操做順序

雙向鏈表

在current後插入element的代碼爲:

element.prior = current;
element.next = current.next;
current.next.prior = element;
current.next = element;

刪除操做相對比較簡單,刪除current結點的代碼爲:

current.prior.next = current.next;
current.next.prior = current.prior;
current = null;

雙向鏈表相對於單鏈表來講佔用了更多的空間,但因爲其良好的對稱性,使得可以方便的訪問某個結點的先後結點,提升了算法的時間性能。是用空間換時間的一個典型應用。

四、靜態鏈表

用數組描述的鏈表叫靜態鏈表,它是那些沒有指針和引用的語言,如Basic、Fortran等,實現鏈表的方式。因爲如今的高級程序語言,通常都擁有指針或引用,可使用更靈活的指針或引用來實現動態鏈表,因此對於靜態鏈表僅掌握其算法思想便可。

靜態鏈表的思想:

  • 讓數組的每一個元素有兩個數據域data和cur組成,其中data用來存放數據元素,cur用來存放元素的後繼在數組中的下標。咱們把cur稱爲遊標。

  • 一般把數組中未被使用的位置稱爲備用鏈表,而數組的第一個位置(下標爲0的位置)的cur存放備用鏈表的第一個結點的下標;數組的最後一個位置的cur則存放第一個有元素的位置的下標,至關於鏈表的頭結點做用。

靜態鏈表狀態圖:

靜態鏈表

代碼以下:

import java.util.*;
public class StaticList<E> implements List<E>, Iterable<E> {

    private static final int DEFAULT_CAPACITY = 100;
    private int size;
    private Node[] nodes;

    private class Node {
        E element;
        int cur;
    }

    public StaticList() {
        initList();
        
    }

    @SuppressWarnings("unchecked")
    private void initList() {
        size = 0;
      //注意這句,不能直接new Node[DEFAULT_CAPACITY],java不容許建立泛型數組
        nodes = new StaticList.Node[DEFAULT_CAPACITY]; 
        for(int i=0; i<nodes.length; i++) {
            nodes[i] = new Node();
            nodes[i].cur = i + 1;
        }
        nodes[nodes.length - 1].cur = 0;
    }

    public int size() { return size;}

    public boolean isEmpty(){ return size == 0;}

    public void clear(){
        initList();
    }

    public void add(E element){ 
        add(0, element);
    }

    //在index插入element
    public void add(int index, E element){
        if(index < 0 || index > size) {
            throw new IndexOutOfBoundsException("參數輸入錯誤"); 
        }
        Node prior = location(index);
        int newCur = malloc();
        if(newCur == 0) {
            throw new RuntimeException("順序表已滿,沒法添加");
        }
        nodes[newCur].element = element;
        nodes[newCur].cur = prior.cur;
        prior.cur = newCur;
        size++;
    }

    //找到第index個結點前的結點
    private Node location(int index){
        Node prior = nodes[nodes.length - 1];
        for(int i=0; i<index; i++) {
            prior = nodes[prior.cur];
        }
        return prior;
    }

    //分配空間,若備用鏈表非空,返回分配的結點的下標,不然返回0
    private int malloc() {
        int i = nodes[0].cur;
        if(i != 0) {
            nodes[0].cur = nodes[i].cur; //備用鏈表的下一個位置
        }
        return i;
    }

    //將下標爲k的空閒結點回收到備用鏈表
    private void free(int index) {
        nodes[index].cur = nodes[0].cur;
        nodes[0].cur = index;
    }

    //刪除元素
    public E delete(int index){
        if(isEmpty()) {
            throw new RuntimeException("順序表爲空,沒法刪除"); 
        }
        if(index < 0 || index >= size) {
            throw new IndexOutOfBoundsException("參數輸入錯誤"); 
        }
        Node prior = location(index);
        int temp = prior.cur; //要刪除元素的下標
        prior.cur = nodes[temp].cur;
        E result = nodes[temp].element;
        nodes[temp].element = null;
        size--;
        free(temp);
        return result;
    }

    public E get(int index){
        if(index < 0 || index >= size) {
            throw new IndexOutOfBoundsException("參數輸入錯誤"); 
        }
        return location(index + 1).element;
    }

    @Override
    public Iterator<E> iterator() {
        return new Iterator<E>() {
            int temp = nodes[nodes.length - 1].cur;
            @Override
            public E next(){  
                E result = nodes[temp].element;  
                temp = nodes[temp].cur;          
                return result;
            }

            @Override
            public boolean hasNext() {
                return temp != 0;
            }
        };
    }
    
    //測試
    public static void main(String[] args){
        StaticList<Integer> sl = new StaticList<Integer>();
        for(int i=0;i<10;i++) {
            sl.add(i);
        }
        System.out.println("刪除1位置元素:"+sl.delete(1));
         sl.add(1,15);
        for(int i=0;i<sl.size();i++) {
            System.out.print(sl.get(i)+" ");
        }
    }
}

爲了實現數組空間的循環利用,靜態鏈表將全部未被使用過的及已經被刪除的元素空間用遊標鏈成一個備用的鏈表。每當插入時就從備用鏈表上取第一個結點做爲待插入的新結點,刪除時將結點回收到備用鏈表中。上面代碼中的malloc()和free()方法分別對應了這兩種操做。靜態鏈表的插入和刪除等操做和單鏈表相似,僅需注意結點的cur爲一個int變量,具體操做能夠參考上面的代碼。

另外須要注意:靜態鏈表初始化時須要建立一個內部類泛型數組StaticList .Node[ ],咱們都知道,java中不能建立泛型數組,一種解決方案是先建立一個Object類型的數組,而後再強轉爲須要的類型。如:

nodes = (Node[])new Object[DEFAULT_CAPACITY];

可是在上面的代碼中,使用這種方法運行時會報ClassCastException,解決方法是

nodes = new StaticList.Node[DEFAULT_CAPACITY];

這樣就能夠解決這個問題,剩下一個未受檢的警告使用@SuppressWarnings("unchecked")註解消除便可。

靜態鏈表有優缺點:

  • 優勢:插入刪除操做時,只須要修改遊標,無需移動元素

  • 缺點:須要事先預估鏈表的容量;不能隨機讀取元素;須要人爲的管理數組的分配(相似於管理內存分配),失去了java語言的優勢。

總的來講,靜態鏈表是爲沒有指針的語言設計的一種實現鏈表的方法,儘管可能用不上,但掌握其設計思想仍是頗有必要的。


總結一下吧,這節主要介紹了線性表兩種不一樣結構(順序存儲結構和鏈式存儲結構)的實現方法,它們是其餘數據結構的基礎,也是如今企業面試中最常考的數據結構類型之一。下一篇我將總結一下線性表中關於鏈表最常考的面試題,感興趣的能夠查看個人下一篇博客 面試題(一),鏈表

相關文章
相關標籤/搜索