死磕算法第二彈——棧、隊列、鏈表(5)

本文整理來源 《輕鬆學算法——互聯網算法面試寶典》/趙燁 編著面試

鏈表其實也可使用數組模擬

在C或者C++語言中有「指針」的概念。由於這個概念,鏈表在編程語言中可以方便地得以發揮做用,但並非全部的編程語言中都有這個指針概念,好比Java。雖然沒有「指針」這個概念,可是Java有「引用」的概念,相似於指針,能夠用於完成鏈表的實現。算法

但如有的編程怨言沒有指針怎麼辦呢?那麼就能夠用數組模擬。編程

靜態鏈表

鏈表有兩種:靜態列表和動態列表。平時所用的鏈表就是動態鏈表,空間都是須要時動態生成的;而靜態鏈表通常是使用數組來描述的鏈表,多數用於一些沒有指針的高級編程語言實現鏈表。數組

靜態列表的實現

通常來講,靜態鏈表的實現就是使用一段固定長度的數組,其中的每一個元素須要有兩個部分組成:一個是data,用於記錄數據,一個是cur,用於記錄指向下一個節點的位置。在C語言中通常使用結構體,在Java中通常使用對象。若是不使用這種組合方式,那麼也可使用兩個數組:一個數組村data,一個數組村cur,讓同一個元素的data和cur的座標保持一致。編程語言

其中靜態鏈表也能夠模擬雙向鏈表,只須要再增長一個部分,用於記錄前面的一個節點的位置便可,可是這個靜態鏈表的維護成本更高。性能

靜態鏈表中的結構體:學習

public class Element<E>{
        private E data;
        private int cur;

        public E getData() {
            return data;
        }

        public void setData(E data) {
            this.data = data;
        }

        public int getCur() {
            return cur;
        }

        public void setCur(int cur) {
            this.cur = cur;
        }
    }

動態鏈表主要包含建立、插入、刪除、遍歷操做;靜態鏈表是使用數組對動態鏈表進行模擬,固然也能夠實現這種操做。測試

建立靜態鏈表

首先要建立。對於動態鏈表來講,建立操做並不複雜;可是對於靜態鏈表來講,建立操做會複雜一些。this

首先須要三個標記,爲了方便,咱們直接採用三個變量來記錄,分別是頭指針標記、尾指針標記和未使用鏈表的頭指針標記。指針

未使用鏈表的頭指針標記的做用是什麼?因爲使用數組做爲鏈表的存儲空間,因此鏈表的元素確定會分佈在數組的一些元素上去,可是對於鏈表進行插入操做時,會出現鏈表的順序和數組的下表順序不一致的狀況,可能會致使數組中的一些連續空間爲空,即未被使用,多是這裏的元素以前被刪除了。

因此咱們須要把鏈表中未使用的空間經過一個鏈表串起來,這樣在須要分配空間時就能夠把未使用鏈表的頭指針指向的元素給咱們真正使用的鏈表了。

這個未使用的鏈表通常叫作備用鏈表,用於串聯那些沒有被使用的數組元素,爲接下來的鏈表中的插入的操做使用,而在鏈表中刪除元素時,須要及時把要刪除的元素加入備用鏈表的頭部記錄下來。

因此在建立一個鏈表時須要把這個備用鏈表串一下。

public StaticLink(int capacity) {
        elements = new Element[capacity];
        unUsed = 0;
        for (int i = 0; i < capacity - 1; i++) {
            elements[i] = new Element<T>();
            elements[i].setCur(i + 1);
        }
        elements[capacity - 1] = new Element<T>();
        elements[capacity - 1].setCur(-1);
    }

建立靜態鏈表時,所須要把數組的全部元素遍歷一下,用備用鏈表穿起來。咱們在這裏對除最後一個元素外的全部元素進行循環賦值,對最後一個元素須要賦不同的值,即把它的指針賦值爲-1,用於說明沒有下一個元素了。

插入操縱

靜態鏈表的頭插入須要進行以下操做。

首先從備用鏈表頭中拿出一個元素,把備用鏈表的投標及指向備用鏈表的第二個元素的數組下標,而後把這個被拿出的元素的cur設爲鏈表頭標記的位置,即當前鏈表中的第1個元素的數組下標,接着把頭元素的標記指向這個新數組元素下標。如此完成了對鏈表頭插入。

靜態鏈表的尾插入相似,首先從備用鏈表頭拿出一個元素,把備用鏈表的頭標記指向備用鏈表的第2個元素的數組下標,由於要做爲鏈表的最後一個元素,所以把這個元素的cur設爲空,接着把真是鏈表的爲指針指向這個數組元素cur設爲這個被拿出來的元素的下標,接着把爲指針標記的值設爲這個元素的數組下標,這樣就完成了鏈表的尾插入。

鏈表的中間插入須要對靜態鏈表進行遍歷,在遍歷到指定位置以後進行操做。備用鏈表一樣從頭袁旭做爲鏈表的插入元素空間。而在鏈表遍歷到要插入元素的位置的前一個元素以後,把這個元素的cur設爲新拿出來的備用元素的下標。而這個新拿出來的備用元素的cur一樣須要設置爲前一個cur的值(也就是本該是新插入這個元素的下一個元素的數組下標),這樣就完成了鏈表的中間插入。

其實靜態鏈表的插入操做和動態鏈表的原理同樣,只是改變了一些操做步驟:一個須要處理靜態鏈表;一個是沒有指針,因此須要修改cur值爲指定元素的數組下標。

刪除操做

靜態鏈表的刪除操做的原理相似於動態鏈表,須要改變先後元素的指針方向,同時把當前元素移出(在靜態鏈表中,就是在備用鏈表中進行頭插入)。

頭刪除時,須要把頭指針的值(head)設爲本來鏈表的第2個元素的數組下標,同時須要把這個被刪除元素的cur設爲備用鏈表的頭元素(unUseHead)數組下標,而後修改備用鏈表頭標記值爲這個元素額數組下標。即刪除靜態鏈表時,除了須要把鏈表cur的關係設定好,還須要把這個被刪除的元素歸還到備用鏈表裏,以備之後使用。

尾刪除時,須要把爲指針(tail)前移,單因爲不知道前一個元素的下標是什麼(除非使用雙向鏈表),因此尾刪除和中間刪除同樣,都須要進行遍歷。在遍歷到要刪除的元素的前一個元素時,把這個元素的cur設爲要刪除的元素的後一個元素的數組下標(若是沒有,則設置爲空,這時刪除的這個元素確定是鏈表最後的一個元素)。若是要刪除的元素時最後一個元素,那麼須要修改微元素的標記(tail)的值。

遍歷操做

插入和刪除操做有時候須要遍歷到鏈表的制定位置。遍歷操做時不須要理會備用鏈表,只須要從頭標記(head)的值開始,找到元素數組的下標,再根據每一個元素的cur去找下一個元素的數組座標,知道cur爲空爲止,則說明遍歷完成。當咱們須要遍歷指定的位置時,須要一個計數器來記錄咱們遍歷了多少個元素。

靜態鏈表不論是插入仍是刪除,其操做步驟都與動態鏈表相似,惟一須要額外處理的就是對備用鏈表的操做。進行插入操做時須要對備用鏈表進行頭刪除;而進行刪除操做時,則須要對備用鏈表進行航插入,這是須要額外維護的工做。

public class StaticLink<T> {

    private Element[] elements;

    private int unUsed;

    private int head;

    private int tail;

    private int size;

    public StaticLink(int capacity) {
        elements = new Element[capacity];
        unUsed = 0;
        for (int i = 0; i < capacity - 1; i++) {
            elements[i] = new Element<>();
            elements[i].setCur(i + 1);
        }
        elements[capacity - 1] = new Element<>();
        elements[capacity - 1].setCur(-1);
    }

    public void insert(T data, int index) {
        if (index == 0) {
            insertFirst(data);
        } else if (index == size) {
            insertLast(data);
        } else {
            checkFull();
            //獲取要插入的元素的前一個元素
            Element<T> preElement = get(index);
            //獲取一個未被使用的元素做爲要插入的元素
            Element<T> unUsedElement = elements[unUsed];
            //記錄要插入元素的數組下標
            int temp = unUsed;
            //將要備用鏈表中拿出來的元素的數組下標設爲備用鏈表頭
            unUsed = unUsedElement.getCur();
            //將要插入元素的指針設爲本來前一個元素的指向的下標值
            unUsedElement.setCur(preElement.getCur());
            //將前一個元素的指針指向插入的元素下標
            preElement.setCur(temp);
            //賦值
            unUsedElement.setData(data);
            //鏈表長度+1
            size++;
        }
    }

    public void printAll() {
        Element<T> element = elements[head];
        System.out.println(element.getData());
        for (int i = 1; i < size; i++) {
            element = elements[element.getCur()];
            System.out.println(element.getData());
        }
    }

    public int size() {
        return size;
    }

    public Element<T> get(int index) {
        checkEmpty();
        Element<T> element = elements[head];
        for (int i = 0; i < index; i++) {
            element = elements[element.getCur()];
        }
        return element;
    }

    public void checkFull() {
        if (size == elements.length) {
            throw new IndexOutOfBoundsException("數組不夠長了");
        }
    }

    public void deleteFirst() {
        checkEmpty();
        Element deleteElement = elements[head];
        int temp = head;
        head = deleteElement.getCur();
        deleteElement.setCur(unUsed);
        unUsed = temp;
        size--;
    }

    public void deleteLast() {
        delete(size - 1);
    }

    public void delete(int index) {
        if (index == 0) {
            deleteFirst();
        } else {
            checkEmpty();
            Element pre = get(index - 1);
            int del = pre.getCur();
            Element deleteElement = elements[del];
            pre.setCur(deleteElement.getCur());
            if (index == size - 1) {
                tail = index - 1;
            }
            deleteElement.setCur(unUsed);
            unUsed = del;
            size--;
        }
    }

    public void checkEmpty() {
        if (size == 0) {
            throw new IndexOutOfBoundsException("鏈表爲空");
        }
    }

    public void insertLast(T data) {
        checkFull();
        Element<T> unUsedElement = elements[unUsed];
        int temp = unUsed;
        unUsed = unUsedElement.getCur();
        elements[tail].setCur(temp);
        unUsedElement.setData(data);
        tail = temp;
        size++;
    }

    public void insertFirst(T data) {
        checkFull();
        Element<T> unUsedElement = elements[unUsed];
        int temp = unUsed;
        unUsed = unUsedElement.getCur();
        unUsedElement.setCur(head);
        unUsedElement.setData(data);
        head = temp;
        size++;
    }


    public class Element<V> {
        private V data;
        private int cur;

        public V getData() {
            return data;
        }

        public void setData(V data) {
            this.data = data;
        }

        public int getCur() {
            return cur;
        }

        public void setCur(int cur) {
            this.cur = cur;
        }
    }

}

測試代碼

public class StaticLinkTest {

    @Test
    public void main(){
        StaticLink<Integer> link = new StaticLink<>(10);
        link.insertFirst(2);
        link.insertFirst(1);
        link.insertLast(4);
        link.insertLast(5);
        link.insert(3,1);
        link.printAll();
        link.deleteFirst();
        link.deleteLast();
        link.delete(1);
        link.printAll();
        Assert.assertEquals(4,(int)link.get(1).getData());
        link.deleteFirst();
        link.deleteFirst();
        Assert.assertEquals(0,link.size());
    }

}

靜態列表的特色

靜態列表的大多數狀況下和動態鏈表類似,可是靜態鏈表的實現方式當值鏈表失去了原有的優點,並且操做變得更加複雜了,主要體現爲與如下幾點。

  1. 空間須要連續申請,並且空間有限的。

因爲靜態鏈表使用數組模擬的,因此空間是連續的,雖然鏈表在數組中能夠不按照順序排列,可是對於整個存儲空間來講,仍是須要連續的。

另外,因爲用到數組模擬,因此咱們在建立數組時須要初始化長度,鏈表自己的一個優勢就是動態添加,可是靜態鏈表沒有辦法這樣添加。當鏈表的長度須要大於數組的長度時就沒法實現數組模擬了,除非複製到一個更長的新數組,那是就須要考慮更多問題,好比串聯備用鏈表、更高性能消耗等。

  1. 查找元素須要遍歷查詢

這點和動態鏈表類似,在找一個元素時須要對鏈表進行遍歷。

  1. 操做更復雜

因爲執行操做須要額外維護一個備用鏈表,因此不管是插入仍是刪除,都須要額外關心操做元素的動向,因此靜態鏈表操做比動態鏈表更復雜。

存在以上問題,因此靜態鏈表除了有助於咱們分析問題,在不少語言中並不經常使用,尤爲是如今不支持指針或者引用高級編程語言愈來愈少的狀況下。可是,咱們學習算法時,仍是須要帳戶哦靜態鏈表的。

相關文章
相關標籤/搜索