数据结构与算法--双向链表

数据结构与算法--双向链表

单向链表的指向是单向的,当前结点只指向它的后一个结点。同样,遍历的时候也只有一个顺序。如果需要访问前一个结点,即使是单向循环链表也需要循环size - 1次。有没有办法更方便的访问前面的结点呢?改变下Node的数据结构就行了。

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

其中Item是泛型参数。现在每个结点都有两个指向了,同时拥有了前后两个结点的信息。由于我们实现的是非循环结构的双向链表,所以firstprev指向null,lastnext指向null,这应该是不言自明的。虽然每个结点有个两个指针,但是很多操作中只用一个指针也够了,所以单链表的很多方法可以直接拿过来使用。

上代码。

package Chap3;

import java.util.Iterator;

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

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

    // 指向第一个节点
    private Node first;
    // 指向最后一个节点
    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 {
            // 因为有prev,所以定位到当前结点就好,如果使用index(index -1)当index为0时报错
            Node current = index(index);
            Node pre = current.prev;
            Node a = new Node();
            a.data = item;
            /* 由于多处使用到current即pre.next,所以最后才改变其值
               1. 先确定新结点的两头
               2. 更新 后结点的前驱
               3. 更新 前结点的后继
            */
            a.prev = pre;
            a.next = current;
            current.prev = a;
            // 如果是insert(0,item)则pre为null,没有前结点,跳过步骤3
            if (pre == null) {
                first = a;
            } else {
                pre.next = a;
            }
            N++;
        }
    }


    public Item remove(int index) {
        /*
            定位到当前位置
            1. 前一结点的后继为下一结点
            2. 下一结点的前驱为前一结点
         */
        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;
        // 如果删除的是第一个结点,则pre为null。没有后继,跳过
        if (pre == null) {
            first = next;
        } else {
            pre.next = next;
        }
        // 如果删除的是最后一个结点,则next为null。没有前驱,跳过
        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应该指向null,但是新的结点next默认就是null
        // 如果是第一个元素,则last和first指向同一个,即第一个
        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一样,第一个元素加入时,last和first指向同一个结点
        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--;
        // 最后一个元素被删除,first自然为空了,但是last需要置空。
        if (isEmpty()) {
            last = null;
        } else {
            // next的引用给first,此时first的prev不为空。需要把表头的前驱设为null(因为first没有前驱)
            first.prev = null;
        }
        return item;
    }

    public void clear() {
        while (first != null) {
            Node next = first.next;
            // 下面两行帮助垃圾回收
            first.next = null;
            first.data = null;
            first = next;
        }
        // 所有元素都空时,last也没有有所指了。记得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);
        }
    }
}

考虑到效率问题,在定位函数Node index(int index)中使用了prev指针。可以看到,当索引位置在链表的前半部分时,使用next指针正向查找。当索引位置在链表的后半部分时,使用prev指针反向查找。这能减少循环次数,提高效率。

再看push方法,与单链表的方法相比多了一句oldfirst.prev = first,让原first结点的前驱指向新的first结点。

pop方法多了一个判断分支,当被弹出的时最后一个结点时,即first被弹出时,由于first.prevfirst.next都已经是null了,所以只需将last置为null;但是如果被弹出的不是最后一个结点呢?next结点携带着prev的信息,所以在 first = next之后,firstprev并不为null。所以需要手动令first.prev = null

add方法,也只是多了一句last.prev = oldlast;,目的是让新的last的前驱指向旧的last结点。

以上三个方法,看图:

再看关键的insert方法,在单链表的实现中,需要定位到插入结点的前一个位置,即index - 1处。由于没有使用头结点,所以在insert(0, item)处,由于不能定位到index为-1的位置,操作方法有所不同(直接使用push方法)。在双向链表中,如果使用单链表的方法,在表尾a[N]处插入时,会定位到last,那么current.next为null,调用current.next.prev将引发空指针异常。除非再单独开一个条件当在a[N]处插入时调用add方法插到末尾。这样代码显得很臃肿。

双向链表有个prev指针,好好利用起来,在定位的时候不必定位到index - 1处了,直接定位到index处可以省去一些麻烦。看insert中这两句

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

这两句代码很机智,在插入第一个结点时若使用insert(0, item),会直接调用add方法,以后在末尾插入时候,依然调用add方法。如果按照单链表insert的思路来实现双链表的insert方法,需要分index == 0index == N等情况。由于可以直接定位到index位置,定位的范围是[0, N],index==N的情况采用上面两行的方法处理。当index == 0时else分支也能正确处理。整个insert其实只使用了插入点处的结点current和它前一个结点current.prev

再看,赋值语句顺序不能搞错。

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,所以最后才改变其值,不然先改变了,后面使用时指向的对象已经变了。这会导致操作失败。记住下面的操作顺序,一般就不会出错了。

1. 先确定新结点的两头
2. 更新 后结点的前驱
3. 更新 前结点的后继

如果是insert(0, item),相当于push操作,此时pre为null,无法调用pre.next,跳过步骤3。想想push的原理,其实就是让插入的结点成为新的first

remove方法,也是定位到删除位置,范围是[0, N - 1]。使用到了移除处的结点current,它前一个结点current.prev以及它后一个结点current.next。先用一个临时变量保存后两者。然后清空移除处结点的信息,帮助垃圾回收。

移除需要分三种情况:

  1. 移除除first和last之外的其他结点

记住下面的操作

-  前一结点的next指向下一结点
-  下一结点的prev指向前一结点

用代码表示就是

pre.next = next;
next.prev = pre;
  1. 移除first结点
- 让下一结点成为新的first
- 下一结点(已经成为first)的prev置为null(pre此时为null)

代码表示为

// pre == null
first = next;
next.prev = pre;
  1. 移除last结点
- 前一个结点(即将称为新的last)的next先指向null(next此时为null)
- 让这个结点成为新的last

代码表示就是

pre.next = next;
last = pre;

其余方法如indexOfcontainscleariterator都直接使用单链表的实现就行。由于双向链表可以访问前面的结点,所以新增了一个reversed方法,返回一个逆序的可迭代对象,即返回一个Iterable<Item>,为此需要实现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;
    }
};

将当前指针移动到last结点位置,然后通过cur = cur.prev遍历。其实就是将单链表iterator实现中的first改成了lastcur.next改成了prev


by @sunhaiyu

2017.8.2

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,233评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,013评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,030评论 0 241
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,827评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,221评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,542评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,814评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,513评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,225评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,497评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,998评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,342评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,986评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,055评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,812评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,560评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,461评论 2 266

推荐阅读更多精彩内容