數(shù)據(jù)結(jié)構(gòu)與算法--線性表的順序存儲(chǔ)結(jié)構(gòu)

數(shù)據(jù)結(jié)構(gòu)與算法--線性表的順序存儲(chǔ)結(jié)構(gòu)

線性表是一個(gè)序列,可以想象成一群有先后順序的的元素集合。線性表是有限序列,所以存在一個(gè)開(kāi)頭和結(jié)尾。開(kāi)頭的元素有且只有一個(gè)后繼,結(jié)尾的元素有且只有一個(gè)前驅(qū)。中間的元素分別有一個(gè)前驅(qū)和后繼。每個(gè)元素都清楚它們的前驅(qū)和后繼是哪個(gè)元素,因此形成了順序。稍微復(fù)雜的線性表,每個(gè)元素元素可以由多個(gè)數(shù)據(jù)項(xiàng)組成。比如鏈表的Node,Node是一個(gè)數(shù)據(jù)元素,每個(gè)Node都含有data(存放的數(shù)據(jù))和next(指向下一元素的指針)組成,雙向鏈表還有prev(指向上一個(gè)元素的指針)。

線性表中存儲(chǔ)的元素類(lèi)型一般相同。比如ArrayList<String>里就只能存String,而且通常我們處理的序列它們?cè)仡?lèi)型相同的情況居多。為此需要引用泛型,比如用Item表示某一不確定的類(lèi)型,但是在之后的處理中,只接受Item或者Item的子類(lèi)(多態(tài))。這樣就保證了我們存入的元素是同一類(lèi)型的。

線性表的順序存儲(chǔ)結(jié)構(gòu)

這里說(shuō)的順序,指的是內(nèi)存空間上的順序,即劃分出一塊連續(xù)的空間,就好比售票口開(kāi)設(shè)了一個(gè)窗口。人們排成一列;而且要求數(shù)據(jù)元素類(lèi)型一致(放一本書(shū)在那兒替你排隊(duì),自己跑去瀟灑,誰(shuí)都不會(huì)同意)。這樣的數(shù)據(jù)存儲(chǔ)方式我們很容易就想到一維數(shù)組。Java中數(shù)組的初始化必須指定長(zhǎng)度。指定多大的長(zhǎng)度呢?由于數(shù)據(jù)量未知,指定容量過(guò)大就浪費(fèi)空間,容量小了又裝不了多少元素。在剛開(kāi)始初始化時(shí)就難住我們了。有一種做法是,如果能估計(jì)數(shù)據(jù)量,那么指定一個(gè)最大容量比估計(jì)的數(shù)據(jù)量適當(dāng)大些,留點(diǎn)余地。最大容量在初始化時(shí)指定,如String[] = new String[500],指定了最多裝500個(gè)元素。用編程語(yǔ)言表達(dá)即是capacity = 500;至于究竟裝了多少個(gè),用size表示實(shí)際存入的元素個(gè)數(shù)。顯然size <= capacity。還有一種做法比較懶,不用操心數(shù)組裝不下的問(wèn)題:等到不夠了,我立馬增大容量;等到空閑的空間太多了,我立馬釋放一些。由于常常我們面對(duì)的數(shù)據(jù)量未知,這不失為一種好方法。

順序存儲(chǔ)結(jié)構(gòu),關(guān)鍵是劃分一塊連續(xù)的地址空間,連續(xù)意味著什么?假如一個(gè)序列從a0到an,每個(gè)元素占據(jù)c個(gè)存儲(chǔ)單元。如果我知道了任何一個(gè)元素的地址,比如a3處,其在內(nèi)存中的位置是locate(a3)。那么a8在內(nèi)存中的位置可以立刻算出為locate(a8) = locate(a3) + (8-3)c,更一般地locate(ai) = locate(a0) + ic。這說(shuō)明我只需定位第一個(gè)元素,后面的元素的位置實(shí)際上已經(jīng)固定下來(lái)了。因?yàn)檫@樣的結(jié)構(gòu),我們?cè)L問(wèn)a[0]和a[5]或者a[n]是一樣復(fù)雜的(或者應(yīng)該叫簡(jiǎn)單)。訪問(wèn)a[i] (0 <= i <= size -1)的時(shí)間復(fù)雜度都是O(1)。

順序存儲(chǔ)結(jié)構(gòu)的特點(diǎn)

想象一個(gè)例子,和你住一條街的鄰居。以自家為a[0],你可以輕松說(shuō)出西邊第3家a[3]是誰(shuí)家,你也能直接定位到西邊第9家a[9]是誰(shuí)家——記憶方式是第i家姓a[i]。如果是這樣記憶的,那么隨便給一個(gè)數(shù)字,你就能脫口而出那個(gè)位置是誰(shuí)家。訪問(wèn)a[i]時(shí)間復(fù)雜度為O(1)。

如果新搬來(lái)一家,最終落戶到你家旁邊,成為你家最新的鄰居(隔開(kāi)了你和你原來(lái)的鄰居),別人在問(wèn)你第5家是誰(shuí)家時(shí),你心里想新來(lái)了一家,原來(lái)的第五家已經(jīng)成為了第六家。實(shí)際上從第一家開(kāi)始,你之前所有的記憶都不適用了,后面的所有位置都變化了,你需要重新記憶一次,如果時(shí)不時(shí)搬來(lái)一家,腦袋可不爆炸了!有人搬家走了也類(lèi)似。插入或者移除在最壞情況下所有元素都要移動(dòng)一次,所以時(shí)間復(fù)雜度為O(n)。

線性表--順序存儲(chǔ)結(jié)構(gòu)的實(shí)現(xiàn)

為了接收多種類(lèi)型,就像ArrayList那樣。使用了泛型,在下面的代碼中用Item表示。同時(shí)實(shí)現(xiàn)了Iterable<Item>使得該類(lèi)是可迭代的,能使用for-each語(yǔ)句。實(shí)現(xiàn)Iterable接口,必須實(shí)現(xiàn)它的iterator()方法,該方法返回一個(gè)Iterator對(duì)象,我使用了匿名內(nèi)部類(lèi)的方式,且實(shí)現(xiàn)Iterator接口的hasNextnext方法。

由于使用到了一維數(shù)組,而且類(lèi)型為泛型Item,我們知道Java中不能直接創(chuàng)捷泛型數(shù)組。下面的寫(xiě)法是錯(cuò)誤的

Item[] a = new Item[capacity];

必須先創(chuàng)建Object數(shù)組,再向下轉(zhuǎn)型為Item類(lèi)型。

Item[] a = (Item[]) new Object[capacity];

另外,該數(shù)組是可調(diào)節(jié)容量的,表長(zhǎng)度即將超過(guò)容量時(shí),自動(dòng)增大;表長(zhǎng)度容量遠(yuǎn)小于容量時(shí),容量減小。

好,先上全部代碼,然后慢慢解釋。

package Chap3;

import java.util.Iterator;

// 實(shí)現(xiàn)Iterable為了使用for-each語(yǔ)句,同時(shí)要實(shí)現(xiàn)iterator方法
public class LinearList<Item> implements Iterable<Item> {
    private int N;
    // 初始化為長(zhǎng)度為1,方便第一次add的時(shí)候可以訪問(wèn)a[0]這個(gè)下標(biāo)
    private Item[] a = (Item[]) new Object[1];

    public LinearList(Item... items) {
        for (int i = 0; i < items.length; i++) {
            add(items[i]);
        }
    }


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

    public int size() {
        return N;
    }

    public Item get(int index) {
        checkRange(index);
        return a[index];
    }

    public void set(int index, Item item) {
        checkRange(index);
        a[index] = item;
    }

    // 先判斷是不是沒(méi)有容量了,若不先增容,會(huì)越界。移位從最后一個(gè)元素開(kāi)始,仔細(xì)想想為什么
    public void insert(int index, Item item) {
        checkRangeForInsert(index);
        if (N == a.length) {
            resize(2 * a.length);
        }
        for (int k = N - 1; k >= index; k--) {
            a[k + 1] = a[k];
        }
        a[index] = item;
        N++;
    }

    // 移除之后再檢查是否長(zhǎng)度太小需要節(jié)約空間,否則先縮小的話,可能導(dǎo)致訪問(wèn)時(shí)越界
    public Item remove(int index) {
        checkRange(index);

        Item item = a[index];
        // 這里就需要正向遍歷了
        for (int k = index; k < N - 1; k++) {
            a[k] = a[k + 1];
        }
        a[N - 1] = null;
        N--;
        if (N > 0 && N == a.length / 4) {
            resize(a.length / 2);
        }
        return item;
    }

    // 先判斷是不是沒(méi)有容量了,若不先增容,會(huì)越界
    public void add(Item item) {
        if (N == a.length) {
            resize(2 * a.length);
        }
        a[N++] = item;
    }


    public int indexOf(Item item) {
        if (item != null) {
            for (int i = 0; i < N; i++) {
                if (item.equals(a[i])) {
                    return i;
                }
            }
        } else {
            for (int i = 0; i < N; i++) {
                if (a[i] == null) {
                    return i;
                }
            }
        }

        return -1;
    }

    public boolean contains(Item item) {
        return indexOf(item) >= 0;
    }

    // N=0但是a.length不為0,可以再次add
    public void clear() {
        for (int i = 0; i < N; i++) {
            a[i] = null;
        }
        N = 0;
    }

    private void resize(int max) {
        Item[] temp = (Item[]) new Object[max];

        for (int i = 0; i < N; i++) {
            temp[i] = a[i];
        }
        // 將容量大于N的數(shù)組傳給a
        a = temp;
    }


    // 檢查數(shù)組下標(biāo)是否越界,注意是N而不是a.length, 因?yàn)閍的容量比N大,訪問(wèn)N之后的也不會(huì)觸發(fā)異常
    // insert的時(shí)候允許向a[N]處插入,這里==N不會(huì)拋出異常
    private void checkRangeForInsert(int index) {
        if (index > N || index < 0) {
            throw new IndexOutOfBoundsException(index + "");
        }
    }

    // 其他情況如remove就不能訪問(wèn)a[N]了
    private void checkRange(int index) {
        if (index >= N || index < 0) {
            throw new IndexOutOfBoundsException(index + "");
        }
    }

    @Override
    public Iterator<Item> iterator() {
        return new Iterator<Item>() {
            private int i = 0;

            @Override
            public boolean hasNext() {
                return i < N;
            }

            @Override
            public Item next() {
                return a[i++];
            }
        };
    }

    @Override
    public String toString() {
        Iterator<Item> it = iterator();
        if (! it.hasNext()) {
            return "[]";
        }

        StringBuilder sb = new StringBuilder();
        sb.append("[");

        while (true){
            Item item = it.next();
            sb.append(item);
            if (! it.hasNext()) {
                return sb.append("]").toString();
            }
            sb.append(", ");
        }
    }

    public static void main(String[] args) {
        LinearList<String> b = new LinearList<>();
        b.add("god");
        b.add("yes");
        b.add("no");
        b.add("man");
        b.insert(0, "ffff");
        System.out.println(b.remove(0)); // ffff
        b.set(1, "ggg");
        System.out.println(b.get(1)); // ggg
        System.out.println(b.indexOf("no")); // 2
        System.out.println(b.size()); // 4
        /* now b have:
        god
        ggg
        no
        man
         */
        System.out.println("*******");
        LinearList<Integer> c = new LinearList<>(1, 2, 3, 4, 5);
        System.out.println(c);
        System.out.println(c.contains(5)); // true
        c.clear();
        c.add(66);

    }
}

數(shù)組容量的調(diào)節(jié)由resize(int max)方法處理。該方法的原理就是創(chuàng)建一個(gè)長(zhǎng)度為max的臨時(shí)數(shù)組,將原數(shù)組的所有數(shù)據(jù)復(fù)制到臨時(shí)數(shù)組,然后將臨時(shí)數(shù)組的引用傳給原數(shù)組。每次要新增元素時(shí),先檢查數(shù)組容量和表長(zhǎng)度是否相等,相等說(shuō)明已經(jīng)沒(méi)有空間存放新來(lái)的元素,故增大容量到原來(lái)的兩倍;類(lèi)似的,每移除一個(gè)元素后,再判斷表長(zhǎng)度是否只有容量的1/4了,若是就縮小容量到原來(lái)的一半。

private Item[] a = (Item[]) new Object[1]之所以指定容量為1,第一是因?yàn)槿萘靠勺詣?dòng)調(diào)節(jié),無(wú)需指定得很大,當(dāng)然不能指定為0。因?yàn)榈谝淮翁砑釉貢r(shí),看add方法一開(kāi)始N == a.length就會(huì)執(zhí)行resize(2 * a.length);如果初始化時(shí)容量設(shè)置為0,resize后還是0。

checkRange方法用來(lái)判斷數(shù)組腳標(biāo)是否越界,訪問(wèn)的index在[0, N - 1]的范圍內(nèi)不會(huì)拋出異常。insert方法中也檢查了數(shù)組腳標(biāo)。不過(guò)使用的是checkRangeForInsert(index)checkRange不同的是,當(dāng)index為N時(shí)也不會(huì)拋出異常,因?yàn)槲覀冊(cè)试S在a[N]的位置插入元素,add方法其實(shí)就是insert(N, item)的簡(jiǎn)寫(xiě)。

接下來(lái)看關(guān)鍵方法insert和remove。

插入時(shí),從最后一個(gè)元素開(kāi)始,向后移動(dòng)一次,接下來(lái)倒數(shù)第二個(gè)元素向后移動(dòng)一次,直到插入點(diǎn)index處向后移動(dòng)一次,結(jié)束移動(dòng)。移動(dòng)的總次數(shù)為N - index。現(xiàn)在插入點(diǎn)空著,在插入點(diǎn)安排新元素就OK了。注意必須是從最后一個(gè)元素開(kāi)始移動(dòng),如果從插入點(diǎn)開(kāi)始移動(dòng),就會(huì)占用別的元素的位置,導(dǎo)致混亂。記住一個(gè)原則:始終朝著空閑的地方移動(dòng)!

移除元素時(shí),也是類(lèi)似的。先彈出要移除的元素,現(xiàn)在這個(gè)位置空閑了,從該位置的下一個(gè)位置開(kāi)始向前移動(dòng)一次,直到最后一個(gè)元素向前移動(dòng)一次,結(jié)束移動(dòng)。移動(dòng)的總次數(shù)為N - index - 1

clear()可以清空表的所有元素,其實(shí)就是將[0, N-1]范圍內(nèi)的所有元素置為null,再將長(zhǎng)度置為0。indexOf(Item item),可以查找item第一次出現(xiàn)的位置,也接受null(因?yàn)閍dd的時(shí)候可以添加null)。原理很簡(jiǎn)單,遍歷查找,找到了就返回當(dāng)前腳標(biāo)。contains(Item item)用到了indexOf,顯然返回的腳標(biāo)不為-1,說(shuō)明存在這個(gè)元素。

還重寫(xiě)了toString方法,可以直接將對(duì)象以列表形式打印出來(lái)。


by @sunhaiyu

2017.7.30

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容