数据结构_链表

目录

一、链表

1.1 链表的概念

1.2 链表的结构

1.3 链表的实现

二、LinkedList

2.1 LinkedList 的实现

三、ArrayList 和 LinkedList 的区别

总结


【顺序表的缺点】

1、在顺序表中插入元素和删除元素时需要移动元素,时间复杂度为O(N)。

2、给顺序表进行扩容时,一般都是进行二倍扩容,若只插入一个元素,就会造成内存的浪费,拷贝数据时也会消耗时间。

由于顺序表具有这些缺陷,那么就会引起思考,有没有一种数据结构可以随用随取,即要添加一个元素,就给一个空间,不造成浪费,同时插入或删除元素时是否可以不移动元素。那就是接下来要介绍的一种新的数据结构——链表。


一、链表

1.1 链表的概念

链表是一种物理存储结构上非连续存储结构,数据元素的逻辑顺序是通过链表中的引用链接次序实现的。

1.2 链表的结构

链表的结构非常多样,但都是由节点组成,节点又分单向节点双向节点,单向节点是由数值域 value 以及 next 域组成,而双向节点比单向节点多一个 prev 域,其中 value 用于存放该节点的值,next 用于存放下一个节点的地址,prev 用于存放上一个节点的地址。

单向与双向节点图:

链表图:

由上文链表图可以看出:链表是在物理上(内存)不一定连续,但在逻辑上是连续的一种结构。而顺序表由于本身是数组,故顺序表是在物理上(内存)一定连续,逻辑上也连续的一种结构。

链表的结构组合总共有 8 种,由三种不同情况组成:单向或双向,带头或者不带头,循环或者非循环。

上文链表图属于单向不带头非循环链表,而单向带头非循环链表(如下图)就是多一个头节点,其中头节点 head 的数值域是无有效数据的,永远标识该链表的头节点,一直不变,而不带头链表若要在最前面插入节点,头节点就会发生改变:

而循环链表就是最后一个节点的 next 域不为 null,放入第一个节点的地址,如单向不带头循环链表:

还有一种常见的链表,该链表是 Java 集合框架库中 LinkedList 的底层实现,双向不带头非循环链表:


1.3 链表的实现

我们来实现一个单向不带头非循环链表,由于链表是由一个个节点组成,所以我们将单节点定义为内部类 ListNode,value 域定义为 int 型,而 next 域中地址是节点的地址,故 next 域类型应该是 ListNode 节点类型。

    static class ListNode {
        public int value;
        public ListNode next;

        public ListNode(int value) {
            this.value = value;
        }
    }

给链表定义一个引用 head ,用于标识第一个节点,默认是 null。

    public ListNode head;//null

生成一个链表,定义四个节点,第一个节点为 node1,将 node2 值的地址放入 node1 的 next 域中,让 node2 成为 node1 的下一个节点,以此类推,构成一个拥有四个节点的链表。例如:

代码实现:

    public void createList() {
        ListNode node1 = new ListNode(12);
        ListNode node2 = new ListNode(23);
        ListNode node3 = new ListNode(34);
        ListNode node4 = new ListNode(45);

        node1.next = node2;//将node2值的地址放入node1的next域
        node2.next = node3;
        node3.next = node4;
        this.head = node1;//head指向第一个节点
    }

1、首先写一个 display() 方法来打印链表中所有节点,方便检查后续方法是否成功实现。

为防止 head 一直改变,导致节点丢失,我们定义一个 cur 节点表示当前节点,cur 从第一个节点开始,利用每个节点的 next 域中的地址往后遍历,直到 cur 节点为 null,遍历打印完成。如图:

代码实现:

    public void display() {
        ListNode cur = head;
        //遍历完链表所有节点
        while (cur != null) {
            System.out.print(cur.value+" ");
            cur = cur.next;//走向下一个节点
        }
    }

 注:display() 方法并不是链表中的方法,只是为了方便检查而编写的。 

2、contains(int key):求当前链表是否存在输入的数据。

定义 cur 节点从头遍历链表,判断每一个节点中 value 值是否与 key 相等,若找到相等的值,则返回 true ;若遍历完链表所有节点都没有找到相等的值,则返回 false。

    public boolean contains(int key) {
        ListNode cur = head;
        //遍历完链表所有节点
        while (cur != null) {
            if (cur.value == key) {
                return true;
            }
            cur = cur.next;//走向下一个节点
        }
        return false;
    }

3、size():求当前链表有多少个节点。

定义一个 count 计数,cur 节点从头遍历链表,经过一个节点计一次数,直至遍历完链表所有节点,返回 count,即链表有 count 个节点。

    public int size() {
        int count = 0;
        ListNode cur = head;
        //遍历完链表所有节点
        while (cur != null) {
            count++;
            cur = cur.next;//走向下一个节点
        }
        return count;
    }

 4、头插法 addFirst(int data):将数据插入链表头部。

将 data 值放入一个 node 节点的 value 域,将原来第一个节点的地址放入 node 节点的 next 域,令 head 指向 node,使 node 成为新的第一个节点,如下图。

代码实现:

    public void addFirst(int data) {
        //将data值放入node节点的value域
        ListNode node = new ListNode(data);
        //将原来头节点的地址放入node的next域
        node.next = this.head;
        //令head指向node
        this.head = node;
    }

5、尾插法 addLast(int data):将数据插入链表尾部。

先判断链表是否为空,若为空,则直接令 head 指向 node;若不为空,则定义 cur 节点从头遍历链表找到最后一个节点,将要插入的 node 节点地址放入尾节点的 next 域中,如下图。

代码实现:

    public void addLast(int data) {
        ListNode node = new ListNode(data);
        //若链表为空
        if(head == null) {
            //head指向node
            head = node;
        } else {    //若不为空
            ListNode cur = head;
            //遍历至链表最后一个节点
            while (cur.next != null) {
                cur = cur.next;
            }
            //将node地址放入尾节点的next域
            cur.next = node;
        }
    }

6、addIndex(int index, int data):将数据插入指定位置。

先判断 index 是否合法,若不合法,则报异常;若合法,则定义 cur 遍历到 index-1 位置上,为防止节点丢失,得先绑定后方节点,故将原 index 位置的节点地址赋予 node 的 next 域,而后将 node 节点的位置赋予 index-1 位置节点的 next 域,即完成插入。如下图。

代码实现: 

    public void addIndex(int index, int data) throws IndexException{
        //判断index是否合法
        if(index < 0 || index > size()) {
            throw new IndexException("index不合法的: "+index);
        }
        ListNode node = new ListNode(data);
        //若index等于0,则用头插法
        if(index == 0) {
            addFirst(data);
            return;
        }
        //若index=size(),则用尾插法
        if(index == size()) {
            addLast(data);
            return;
        }
        //插入
        ListNode cur = searchPrevIndex(index);
        node.next = cur.next;
        cur.next = node;
    }

    private ListNode searchPrevIndex(int index) {
        ListNode cur = head;
        int count = 0;
        //遍历至index-1的位置
        while (count != index-1) {
            cur = cur.next;
            count++;
        }
        return cur;
    }

7、remove(int key):删除第一次出现的指定数据。

先判断链表是否为空,若为空,则无法删除,返回;若不为空,则判断是否第一个节点就是要删除的节点,若是,则 head 指向下一个节点,删除成功;若不是,则定义 cur 遍历至要删除节点的前驱,若遍历完全部节点,还未找到要删除节点,则返回;若找到,则定义一个 del 作为要删除节点,将要删除节点的 next 域的地址放入 cur 的 next 域,这样则成功删除。

    public void remove(int key) {
        //判断链表是否为空
        if(head == null) {
            return;
        }
        //若第一个节点就是要删除的节点,则head指向下一个节点
        if(head.value == key) {
            head = head.next;
            return;
        }
        //找到要删除节点的前驱
        ListNode cur = findPrevKey(key);
        //没有要删除的数字
        if(cur == null) {
            return;
        }
        //删除
        //cur.next = cur.next.next;
        ListNode del = cur.next;
        cur.next = del.next;
    }

    private ListNode findPrevKey(int key) {
        ListNode cur = head;
        //遍历全部节点
        while (cur.next != null) {
            //找到要删除节点的前驱
            if(cur.next.value == key) {
                return cur;
            }else {
                cur = cur.next;
            }
        }
        return null;
    }

 8、removeAllKey(int key):删除所有指定数据。

先判断链表是否为空,若为空,则返回;若不为空,则定义 prev 指向第一个节点,cur指向第二个节点。开始遍历链表,若 cur 指向节点的值要删除的数据,则将 cur 节点下一节点的地址放入 prev 节点的 next 域中;若 cur 指向节点的值不是要删除的数据,则 cur 与 prev 都指向对应的下一节点。删除完除第一个节点以外的所有节点,再开始判断第一个节点中的值是否是需要删除的数据,若一开始就判断,遇到第一第二连续两个节点中都是要删除的数据,除非加入循环来判断,否则就会发生漏删的情况。

    public void removeAllKey(int key) {
        //判断链表是否为空
        if(head == null) {
            return;
        }
        ListNode prev = head;//前驱节点
        ListNode cur = head.next;//当前节点
        //遍历所有节点
        while (cur != null) {
            //删除
            if(cur.value == key) {
                prev.next = cur.next;
                cur = cur.next;
            }else {
                prev = cur;
                cur = cur.next;
            }
        }
        //最后判断第一个节点是否是需要删除的值
        if(head.value == key) {
            head = head.next;
        }
    }

9、clear():清空链表。

令 head 指向空,则全部节点丢失,清空成功。

    public void clear() {
        head = null;
    }

二、LinkedList

2.1 LinkedList 的实现

由于 LinkedList 的底层实现是双向不带头非循环链表,故我们来试着实现该链表。我们将单个双向节点定义为内部类 ListNode,value 域定义为 int 型,而 next 域中地址是后一个节点的地址,故 next 域类型应该是 ListNode 节点类型,prev 域中是前一个节点的地址,故同理。

    static class ListNode {
        public int value;
        public ListNode next;
        public ListNode prev;

        public ListNode(int value) {
            this.value = value;
        }
    }

    //定义head,用于标识第一个节点,默认是null
    public ListNode head;
    //定义last,用于标识最后一个节点,默认是null
    public ListNode last;

由于双向链表与单向链表区别只在于双向链表多了一个 prev 域,故其中一些方法是一致的,例如上文中的 display()、contains() 以及 size() 方法。其余方法也是同样的道理,主要就是要多进行一步 prev 域的操作。

1、头插法 addFirst(int data)

与单链表的头插法类似,首先将 data 值存入一个 node 节点的 value 域,然后判断链表是否为空,如果为空,则链表的 head 节点与 last 节点都为 node 节点;其次将 head 节点的地址存入 node 节点的 next 域中,再将 node 节点的地址存入 head 节点的 prev 域中,最后改变 head 指向。

代码实现:

    public void addFirst(int data) {
        ListNode node = new ListNode(data);
        //如果链表为空
        if (head == null) {
            head = node;
            last = node;
        } else {
            node.next = head;
            head.prev = node;
            head = node;
        }
    }

2、尾插法 addLast(int data) 

与头插法类似,若 head 节点不为空,将 node 节点的地址放入 last 节点的 next 域,再将 last 节点的地址放入 node 节点的 prev 域,最后改变 last 指向,使其指向 node 节点。

    public void addLast(int data) {
        ListNode node = new ListNode(data);
        if (head == null) {
            head = node;
            last = node;
        } else {
            last.next = node;
            node.prev = last;
            last = node;
        }
    }

3、 addIndex(int index, int data):将数据插入指定位置。

首先判断 index 是否合法,若 index 小于 0 或大于链表大小则抛异常。其次判断要插入位置是否为链表的头或者尾,再执行头插法或尾插法。若处于中间位置,则需要先定义一个 node 节点,将要插入数据放入 node 节点,再定义一个 cur 节点,令其遍历至要插入位置,最后进行四步存放地址操作:① 将 cur 节点的地址放入 node 节点的 next 域;② 将 node 节点地址放入 cur 节点的前一个节点 next 域;③ 将 cur 节点的前一个节点地址放入 node 节点的 prev 域;④ 将 node 节点的地址放入 cur 节点的 prev 域。

插入位置为中间位置时实现过程:

代码实现:

    public void addIndex(int index, int data) {
        if (index < 0 || index > size()) {
            throw new IndexException("index不合法");
        } else if (index == 0) {
            //头插法
            addFirst(data);
        } else if (index == size()) {
            //尾插法
            addLast(data);
        } else {
            ListNode cur = findIndex(index);
            ListNode node = new ListNode(data);
            //位于中间位置时
            node.next = cur;
            cur.prev.next = node;
            node.prev = cur.prev;
            cur.prev = node;
        }
    }

    //找到要插入的节点位置
    public ListNode findIndex(int index) {
        ListNode cur = head;
        while (index != 0) {
            cur = cur.next;
            index--;
        }
        return cur;
    }

4、remove(int key):删除第一次出现的 key。

假若要删除值为 34 的节点,首先定义一个当前节点,令其遍历链表,找到所要删除的节点,然后开始判断其属于哪种节点,具有三种情况:① 若要删除节点为头节点,则先将 head 指向下一个节点,然后判断链表中是否只有一个节点,若只有一个节点,则 head 指向 null,也需要将 last 指向 null;若不止一个节点,则将 head 指向下一个节点后,需将 head 节点的 prev 域置空;② 若要删除节点为中间节点,则将 cur 节点的后一个节点地址放入 cur 节点的前一个节点 next 域中,再将 cur 节点的前一个节点地址放入 cur 节点的下一个节点 prev 域即可;③ 若要删除节点为尾节点,则将 cur 节点的前一个节点 next 域置空,再将 last 指向上一个节点即可。

删除节点为中间节点时实现过程:

    public void remove(int key) {
        ListNode cur = head;
        while (cur != null) {
            //找到所要删除的节点
            if (cur.value == key) {
                //若删除的是头节点
                if (cur == head) {
                    head = head.next;
                    //若链表中只有一个节点
                    if (head.next == null) {
                        last = null;
                    } else {
                        //不止一个节点
                        head.prev = null;
                    }
                } else {
                    cur.prev.next = cur.next;
                    if (cur.next != null) {
                        //若删除的是中间节点
                        cur.next.prev = cur.prev;
                    } else {
                        //若删除的是尾节点
                        last = last.prev;
                    }
                }
                return;
            }
            cur = cur.next;
        }
    }

5、removeAllKey(int key):删除所有 key。

假如链表中具有多个值为 34 的节点,而此时想把值为 34 的节点全部删除,若使用 remove() 方法,显然只能删除第一个值为 34 的节点,此时我们可以使用 removeAllKey 方法。而两种方法的实现区别只在于 remove() 方法删除一个节点后就会返回,若我们将 return 删掉,不让其返回,即可实现全部删除。

    public void removeAllKey(int key) {
        ListNode cur = head;
        while (cur != null) {
            //找到所要删除的节点
            if (cur.value == key) {
                //若删除的是头节点
                if (cur == head) {
                    head = head.next;
                    //若链表中只有一个节点
                    if (head.next == null) {
                        last = null;
                    } else {
                        //不止一个节点
                        head.prev = null;
                    }
                } else {
                    cur.prev.next = cur.next;
                    if (cur.next != null) {
                        //若删除的是中间节点
                        cur.next.prev = cur.prev;
                    } else {
                        //若删除的是尾节点
                        last = last.prev;
                    }
                }
            }
            cur = cur.next;
        }
    }

6、clear():清空链表。

令 head 和 last 指向 null,则全部节点丢失,清空成功。

    public void clear() {
        head = null;
        last = null;
    }

三、ArrayList 和 LinkedList 的区别

不同点ArrayListLinkedList
存储空间上物理上一定连续逻辑上连续,物理上不一定连续
随机访问支持;O(1)不支持;O(N)
头插需要移动元素;O(N)只需修改引用指向;O(1)
插入空间不够时需扩容容量无限制
更适用场景查找、修改插入、删除

总结

1、在插入节点的时候,为防止节点丢失,得先绑定插入位置的后方节点,再进行下一步操作。

2、在单向链表中,删除第一次出现的指定数据时,应一开始就判断 head 是否需要删除。

3、在单向链表中删除所有指定数据时,需先删完除 head 以外的所有节点,最后再判断 head 是否需要删除。

4、在双向列表中,remove() 方法与 removeAllKey() 方法区别只在于是否会 return。

  • 21
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值