數(shù)據(jù)結(jié)構(gòu)與算法--雙向鏈表

數(shù)據(jù)結(jié)構(gòu)與算法--雙向鏈表

單向鏈表的指向是單向的,當(dāng)前結(jié)點(diǎn)只指向它的后一個(gè)結(jié)點(diǎn)。同樣,遍歷的時(shí)候也只有一個(gè)順序。如果需要訪問(wèn)前一個(gè)結(jié)點(diǎn),即使是單向循環(huán)鏈表也需要循環(huán)size - 1次。有沒(méi)有辦法更方便的訪問(wèn)前面的結(jié)點(diǎn)呢?改變下Node的數(shù)據(jù)結(jié)構(gòu)就行了。

private class Node {
    Item data;
    Node prev;
    Node next;
}

其中Item是泛型參數(shù)。現(xiàn)在每個(gè)結(jié)點(diǎn)都有兩個(gè)指向了,同時(shí)擁有了前后兩個(gè)結(jié)點(diǎn)的信息。由于我們實(shí)現(xiàn)的是非循環(huán)結(jié)構(gòu)的雙向鏈表,所以firstprev指向null,lastnext指向null,這應(yīng)該是不言自明的。雖然每個(gè)結(jié)點(diǎn)有個(gè)兩個(gè)指針,但是很多操作中只用一個(gè)指針也夠了,所以單鏈表的很多方法可以直接拿過(guò)來(lái)使用。

上代碼。

package Chap3;

import java.util.Iterator;

public class DLinkedList<Item> implements Iterable<Item> {

    private class Node {
        Item data;
        Node prev;
        Node next;
    }

    // 指向第一個(gè)節(jié)點(diǎn)
    private Node first;
    // 指向最后一個(gè)節(jié)點(diǎn)
    private Node last;
    private int N;

    public int size() {
        return N;
    }

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

    private Node index(int index) {
        // [0, N-1]的定位范圍
        if (index < 0 || index >= N) {
            throw new IndexOutOfBoundsException(index + "");
        }
        // 索引在前半部分就正向查找, 在后半部分就反向查找
        if (index < N / 2) {
            Node current = first;
            for (int j = 0; j < index; j++) {
                current = current.next;
            }
            return current;
        } else {
            Node current = last;
            for (int i = N - 1; i > index; i--) {
                current = current.prev;
            }
            return current;
        }
    }

    public Item get(int index) {
        Node current = index(index);
        return current.data;
    }

    public void set(int index, Item item) {
        Node current = index(index);
        current.data = item;
    }

    // 可以在表頭(index==0)和表尾插入
    public void insert(int index, Item item) {
        if (index == N) {
            add(item);
        } else {
            // 因?yàn)橛衟rev,所以定位到當(dāng)前結(jié)點(diǎn)就好,如果使用index(index -1)當(dāng)index為0時(shí)報(bào)錯(cuò)
            Node current = index(index);
            Node pre = current.prev;
            Node a = new Node();
            a.data = item;
            /* 由于多處使用到current即pre.next,所以最后才改變其值
               1. 先確定新結(jié)點(diǎn)的兩頭
               2. 更新 后結(jié)點(diǎn)的前驅(qū)
               3. 更新 前結(jié)點(diǎn)的后繼
            */
            a.prev = pre;
            a.next = current;
            current.prev = a;
            // 如果是insert(0,item)則pre為null,沒(méi)有前結(jié)點(diǎn),跳過(guò)步驟3
            if (pre == null) {
                first = a;
            } else {
                pre.next = a;
            }
            N++;
        }
    }


    public Item remove(int index) {
        /*
            定位到當(dāng)前位置
            1. 前一結(jié)點(diǎn)的后繼為下一結(jié)點(diǎn)
            2. 下一結(jié)點(diǎn)的前驅(qū)為前一結(jié)點(diǎn)
         */
        Node current = index(index);
        Item item = current.data;
        Node pre = current.prev;
        Node next = current.next;
        // 下面三行幫助垃圾回收
        current.prev = null;
        current.next = null;
        current.data = null;
        // 如果刪除的是第一個(gè)結(jié)點(diǎn),則pre為null。沒(méi)有后繼,跳過(guò)
        if (pre == null) {
            first = next;
        } else {
            pre.next = next;
        }
        // 如果刪除的是最后一個(gè)結(jié)點(diǎn),則next為null。沒(méi)有前驅(qū),跳過(guò)
        if (next == null) {
            last = pre;
        } else {
            next.prev = pre;
        }

        N--;
        return item;
    }

    // 表尾加入元素
    public void add(Item item) {
        Node oldlast = last;
        last = new Node();
        last.data = item;
        // last應(yīng)該指向null,但是新的結(jié)點(diǎn)next默認(rèn)就是null
        // 如果是第一個(gè)元素,則last和first指向同一個(gè),即第一個(gè)
        if (isEmpty()) {
            first = last;
        } else {
            last.prev = oldlast;
            oldlast.next = last;
        }
        N++;
    }

    // 表頭插入元素
    public void push(Item item) {
        Node oldfirst = first;
        first = new Node();
        first.data = item;
        // 和add一樣,第一個(gè)元素加入時(shí),last和first指向同一個(gè)結(jié)點(diǎn)
        if (isEmpty()) {
            last = first;
        } else {
            first.next = oldfirst;
            oldfirst.prev = first;
        }
        N++;
    }

    // 刪除表頭元素
    public Item pop() {
        Item item = first.data;
        Node next = first.next;
        // 這兩行有助于垃圾回收
        first.data = null;
        first.next = null;
        first = next;
        N--;
        // 最后一個(gè)元素被刪除,first自然為空了,但是last需要置空。
        if (isEmpty()) {
            last = null;
        } else {
            // next的引用給first,此時(shí)first的prev不為空。需要把表頭的前驅(qū)設(shè)為null(因?yàn)閒irst沒(méi)有前驅(qū))
            first.prev = null;
        }
        return item;
    }

    public void clear() {
        while (first != null) {
            Node next = first.next;
            // 下面兩行幫助垃圾回收
            first.next = null;
            first.data = null;
            first = next;
        }
        // 所有元素都空時(shí),last也沒(méi)有有所指了。記得last置空
        last = null;
        N = 0;
    }

    public int indexOf(Item item) {
        int index = 0;
        if (item != null) {
            for (Node cur = first; cur != null; cur = cur.next) {
                if (item.equals(cur.data)) {
                    return index;
                }
                index++;
            }
        } else {
            for (Node cur = first; cur != null; cur = cur.next) {
                if (cur.data == null) {
                    return index;
                }
                index++;
            }
        }

        return -1;
    }

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

    @Override
    public Iterator<Item> iterator() {
        return new Iterator<Item>() {
            private Node current = first;

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

            @Override
            public Item next() {
                Item item = current.data;
                current = current.next;
                return item;
            }
        };
    }

    public Iterable<Item> reversed() {
        return new Iterable<Item>() {
            @Override
            public Iterator<Item> iterator() {
                return new Iterator<Item>() {
                    Node cur = last;

                    @Override
                    public boolean hasNext() {
                        return cur != null;
                    }

                    @Override
                    public Item next() {
                        Item item = cur.data;
                        cur = cur.prev;
                        return item;
                    }
                };
            }

            @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(", ");
                }
            }
        };
    }

    @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) {
        DLinkedList<Integer> a = new DLinkedList<>();
        a.push(2);
        a.push(1);
        a.push(3);
        a.set(2, 11);
        System.out.println(a.get(2)); // 11
        System.out.println(a);

        a.insert(0, 444);
        a.clear();

        a.add(11);
        a.add(12);
        a.add(13);
        a.push(14);
        a.remove(2); // 12
        a.pop(); // 14
        System.out.println(a.indexOf(13)); // 1


        System.out.println(a.reversed());
        for (Integer aa : a.reversed()) {
            System.out.println(aa);
        }
    }
}

考慮到效率問(wèn)題,在定位函數(shù)Node index(int index)中使用了prev指針。可以看到,當(dāng)索引位置在鏈表的前半部分時(shí),使用next指針正向查找。當(dāng)索引位置在鏈表的后半部分時(shí),使用prev指針?lè)聪虿檎?。這能減少循環(huán)次數(shù),提高效率。

再看push方法,與單鏈表的方法相比多了一句oldfirst.prev = first,讓原first結(jié)點(diǎn)的前驅(qū)指向新的first結(jié)點(diǎn)。

pop方法多了一個(gè)判斷分支,當(dāng)被彈出的時(shí)最后一個(gè)結(jié)點(diǎn)時(shí),即first被彈出時(shí),由于first.prevfirst.next都已經(jīng)是null了,所以只需將last置為null;但是如果被彈出的不是最后一個(gè)結(jié)點(diǎn)呢?next結(jié)點(diǎn)攜帶著prev的信息,所以在 first = next之后,firstprev并不為null。所以需要手動(dòng)令first.prev = null。

add方法,也只是多了一句last.prev = oldlast;,目的是讓新的last的前驅(qū)指向舊的last結(jié)點(diǎn)。

以上三個(gè)方法,看圖:

再看關(guān)鍵的insert方法,在單鏈表的實(shí)現(xiàn)中,需要定位到插入結(jié)點(diǎn)的前一個(gè)位置,即index - 1處。由于沒(méi)有使用頭結(jié)點(diǎn),所以在insert(0, item)處,由于不能定位到index為-1的位置,操作方法有所不同(直接使用push方法)。在雙向鏈表中,如果使用單鏈表的方法,在表尾a[N]處插入時(shí),會(huì)定位到last,那么current.next為null,調(diào)用current.next.prev將引發(fā)空指針異常。除非再單獨(dú)開(kāi)一個(gè)條件當(dāng)在a[N]處插入時(shí)調(diào)用add方法插到末尾。這樣代碼顯得很臃腫。

雙向鏈表有個(gè)prev指針,好好利用起來(lái),在定位的時(shí)候不必定位到index - 1處了,直接定位到index處可以省去一些麻煩???code>insert中這兩句

if (index == N) {
    add(item);
}

這兩句代碼很機(jī)智,在插入第一個(gè)結(jié)點(diǎn)時(shí)若使用insert(0, item),會(huì)直接調(diào)用add方法,以后在末尾插入時(shí)候,依然調(diào)用add方法。如果按照單鏈表insert的思路來(lái)實(shí)現(xiàn)雙鏈表的insert方法,需要分index == 0index == N等情況。由于可以直接定位到index位置,定位的范圍是[0, N],index==N的情況采用上面兩行的方法處理。當(dāng)index == 0時(shí)else分支也能正確處理。整個(gè)insert其實(shí)只使用了插入點(diǎn)處的結(jié)點(diǎn)current和它前一個(gè)結(jié)點(diǎn)current.prev

再看,賦值語(yǔ)句順序不能搞錯(cuò)。

Node pre = current.prev;
Node a = new Node();
a.data = item;

a.prev = pre;
a.next = current;
current.prev = a;

if (pre == null) {
  first = a;
} else {
  pre.next = a;
}

由于多處使用到currentpre.next,所以最后才改變其值,不然先改變了,后面使用時(shí)指向的對(duì)象已經(jīng)變了。這會(huì)導(dǎo)致操作失敗。記住下面的操作順序,一般就不會(huì)出錯(cuò)了。

1. 先確定新結(jié)點(diǎn)的兩頭
2. 更新 后結(jié)點(diǎn)的前驅(qū)
3. 更新 前結(jié)點(diǎn)的后繼

如果是insert(0, item),相當(dāng)于push操作,此時(shí)pre為null,無(wú)法調(diào)用pre.next,跳過(guò)步驟3。想想push的原理,其實(shí)就是讓插入的結(jié)點(diǎn)成為新的first

remove方法,也是定位到刪除位置,范圍是[0, N - 1]。使用到了移除處的結(jié)點(diǎn)current,它前一個(gè)結(jié)點(diǎn)current.prev以及它后一個(gè)結(jié)點(diǎn)current.next。先用一個(gè)臨時(shí)變量保存后兩者。然后清空移除處結(jié)點(diǎn)的信息,幫助垃圾回收。

移除需要分三種情況:

  1. 移除除first和last之外的其他結(jié)點(diǎn)

記住下面的操作

-  前一結(jié)點(diǎn)的next指向下一結(jié)點(diǎn)
-  下一結(jié)點(diǎn)的prev指向前一結(jié)點(diǎn)

用代碼表示就是

pre.next = next;
next.prev = pre;
  1. 移除first結(jié)點(diǎn)
- 讓下一結(jié)點(diǎn)成為新的first
- 下一結(jié)點(diǎn)(已經(jīng)成為first)的prev置為null(pre此時(shí)為null)

代碼表示為

// pre == null
first = next;
next.prev = pre;
  1. 移除last結(jié)點(diǎn)
- 前一個(gè)結(jié)點(diǎn)(即將稱(chēng)為新的last)的next先指向null(next此時(shí)為null)
- 讓這個(gè)結(jié)點(diǎn)成為新的last

代碼表示就是

pre.next = next;
last = pre;

其余方法如indexOf、contains、cleariterator都直接使用單鏈表的實(shí)現(xiàn)就行。由于雙向鏈表可以訪問(wèn)前面的結(jié)點(diǎn),所以新增了一個(gè)reversed方法,返回一個(gè)逆序的可迭代對(duì)象,即返回一個(gè)Iterable<Item>,為此需要實(shí)現(xiàn)iterator方法。

new Iterator<Item>() {
    Node cur = last;

    @Override
    public boolean hasNext() {
      return cur != null;
    }

    @Override
    public Item next() {
      Item item = cur.data;
      cur = cur.prev;
      return item;
    }
};

將當(dāng)前指針移動(dòng)到last結(jié)點(diǎn)位置,然后通過(guò)cur = cur.prev遍歷。其實(shí)就是將單鏈表iterator實(shí)現(xiàn)中的first改成了last,cur.next改成了prev。


by @sunhaiyu

2017.8.2

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 鏈表 概念 說(shuō)到鏈表,coder們都不會(huì)陌生,在日常開(kāi)發(fā)中或多或少都會(huì)用到它。它是鏈?zhǔn)酱鎯?chǔ)的線性表,簡(jiǎn)稱(chēng)鏈表。鏈表...
    扈扈哈嘿閱讀 2,144評(píng)論 0 5
  • 數(shù)據(jù)結(jié)構(gòu)與算法--單鏈表 鏈?zhǔn)酱鎯?chǔ)結(jié)構(gòu)的特點(diǎn) 學(xué)習(xí)線性表的順序存儲(chǔ)結(jié)構(gòu)時(shí),舉了個(gè)例子:記憶和你住一條街的每家姓氏,...
    sunhaiyu閱讀 710評(píng)論 1 0
  • 一、基本數(shù)據(jù)類(lèi)型 注釋 單行注釋?zhuān)?/ 區(qū)域注釋?zhuān)?* */ 文檔注釋?zhuān)?** */ 數(shù)值 對(duì)于byte類(lèi)型而言...
    龍貓小爺閱讀 4,477評(píng)論 0 16
  • 一、線性表的順序存儲(chǔ)設(shè)計(jì)與實(shí)現(xiàn)(順序表) 1.1 順序存儲(chǔ)結(jié)構(gòu)的設(shè)計(jì)原理概要 順序存儲(chǔ)結(jié)構(gòu)底層是利用數(shù)組來(lái)實(shí)現(xiàn)的,...
    千涯秋瑟閱讀 1,598評(píng)論 2 4
  • 數(shù)據(jù)結(jié)構(gòu)與算法--靜態(tài)鏈表 鏈表的實(shí)現(xiàn)依賴(lài)于指針(在Java中稱(chēng)作對(duì)象引用可能更準(zhǔn)確),如果某編程語(yǔ)言沒(méi)有指針呢?...
    sunhaiyu閱讀 1,110評(píng)論 0 4

友情鏈接更多精彩內(nèi)容