【数据结构】关于链表,这一篇就够了!

目录

1.ArrayList的缺陷

2.链表

2.1 链表的概念及结构 

2.2链表的分类

3.链表的实现(无头单向非循环)

 3.1链表的定义

3.2头插法

3.3尾插法

3.3目标位置插入

3.4删除第一次出现的关键字

3.5删除所有值为key的节点

3.6链表的长度

3.7清空链表

3.8打印链表

4.双向链表(无头双向)

4.1链表的定义

4.2头插法

4.3尾插法

4.4目标位置插入

4.4删除第一次出现的关键字

4.5删除所有值为key的节点

4.6其余方法

5.单项链表和双向链表的区别

6.ArrayList 与 LinkedList区别


1.ArrayList的缺陷

继上篇文章所讲顺序表超详解,我们已经了解了顺序表的使用,并进行简单的模拟实现。

由于其底层是一段连续空间,当在ArrayList任意位置插入或者删除元素时,就需要将后序元素整体往前或者往后搬移,时间复杂度为O(n),效率比较低,因此ArrayList不适合做任意位置插入和删除比较多的场景。因此:java集合中又引入了LinkedList,即链表结构。

2.链表

2.1 链表的概念及结构 

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

 

2.2链表的分类

 实际中链表的结构非常多样,以下情况组合起来就有8种链表结构:
1. 单向或者双向


2. 带头或者不带头

 

 3. 循环或者非循环

 虽然有这么多的链表的结构,但是我们重点掌握两种:
无头单向非循环链表:结构简单,一般不会单独用来存数据。
无头双向链表:在Java的集合框架库中LinkedList底层实现就是无头双向循环链表。

3.链表的实现(无头单向非循环)

 3.1链表的定义

public static class Node {
        int value;//值域
        Node next;//指针指向下一个节点
        public Node(int value) {

            this.value = value;
        }
    }
    public Node head; //定义一个head,便于记录

3.2头插法

顾名思义就是在链表的最前端插入新的元素

如图所示头插法的具体演示

public void addFirst(int data){
        Node node = new Node(data);
        node.next = head;
        head = node;
    }

3.3尾插法

如图尾插法具体演示

public void addLast(int data){
        Node node = new Node(data);
       //判断是不是空链表,若是空链表,将node赋给头节点head即可
        if (head == null){
            head = node;
            return;
        }
        //设置变量current保存head,防止head丢失
        Node current = head;
       //循环找到最后一个节点
        while(current.next != null){
            current = current.next;
        }
        current.next = node;
    }

3.3目标位置插入

1)首先检查所传入的下标是否合法

private void checkRangeIndex(int index) {
        if (index < 0 || index > size()){
            throw new IndexOutOfBoundsException("下表不合法,index = " + index);
        }
    }

2)我们需要找到具体插入的位置并记录

private Node findPrevNodeByIndex(int index) {
        //记录头节点并进行遍历
        Node current = head;
        for (int i = 0; i < index - 1; i++) {
            current = current.next;
        }
        return current;
    }

3)找到位置我们需要判断是不是头节点或者尾节点还是中间节点,位置不同具体操作不同

public void addIndex(int index,int data){
        //检查下标的合法性
        checkRangeIndex(index);
        // 此位置为链表头位置,用头插法即可
        if (index == 0){
            addFirst(data);
            return;
        }
        if (index == size()) {
        //此位置为链表尾位置,用尾插法即可
            addLast(data);
            return;
        }
        // 找到插入位置
        Node prevIndex = findPrevNodeByIndex(index);
        Node node = new Node(data);
        //插入操作
        node.next = prevIndex.next;
        prevIndex.next = node;

    }

3.4删除第一次出现的关键字

我们要遍历整个链表,来比较确定所要删除的第一个值

 public void remove(int key){
        //头节点就是要删除的节点
        if(head.value == key){
            head = head.next;
            return;
        }

        Node current = head;
        //(current.next != null)此条件判断是否走到最后一个节点 
        while(current.next != null){
            if (current.next.value == key){
               //删除操作
                current.next = current.next.next;
                return;
            }
            //没找到接着遍历
            current = current.next;
        }
     }

3.5删除所有值为key的节点

如下图所示进行删除操作

public void removeAllKey(int key){
        // 判断是不是空链表
        if (head == null){
            return;
        }
        //记录头节点和头节点的下一个节点
        Node prevNode = head;
        Node current = head.next;

        while (current != null){
            if (current.value == key){
        //是删除节点,进行删除
                prevNode.next = current.next;
            }else {
        // 不是删除节点,交换位置继续遍历
                prevNode = current;
            }
            current = current.next;
        }
        //处理头节点
        if (head.value == key){
            head = head.next;
        }
    }

3.6链表的长度

链表的长度我们遍历求得即可,定义一个int类型的变量count记录,返回count即可

public int size(){
        Node current = head;
        int count = 0;
        while (current != null){
            count++;
            current = current.next;
        }
        return count;
    }

3.7清空链表

清空链表可以有两种方法

1)直接置头节点head = null即可

   public void clear() {
    head =  null;
   }

2)设置每个节点为空,遍历并操作,直至整个链表为空

 public void clear() {
        Node current = head;
        while (current != null) {
            Node nextNode = current.next;
            current.next = null;
            current = nextNode;
        }
        head = null;
    }

3.8打印链表

 public void display(){
        StringBuilder sb = new StringBuilder();
        sb.append("[");
        Node current = head;
        while(current != null){
            sb.append(current.value);
            if (current.next != null){
                sb.append(",");
            }
            current = current.next;
        }
        sb.append("]");
        System.out.println(sb.toString());
    }

此时我们已经完成了无头双向非循环链表的实现,与顺序表相比,确实使用起来更加的灵活方便

接下来我们再介绍双向链表

4.双向链表(无头双向)

4.1链表的定义

双向链表顾名思义就是节点之间有两个连接点,一个前驱节点prev,一个后继节点next,相比于单向链表,多了前驱节点,如图所示

 此时head记录链表头,tail记录链表尾

4.2头插法

如图所示的头插法

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

4.3尾插法

 public void addLast(int data){
        ListNode node = new ListNode(data);
        //空链表操作
        if (head == null){
            head = node;
        }else {
            tail.next = node;
            node.prev = tail;
        }
        tail = node;
    }

4.4目标位置插入

如题,各个节点地址的变化就是插入的操作

public void addIndex(int index, int data) {
       // 检查下标的合法性
        checkRangeForAdd(index);
       //链表头直接用头插法
        if(index == 0){
            addFirst(data);
            return;
        }
       // 链表尾直接用尾插法
        if (index == size()){
            addLast(data);
            return;
        }
        
        ListNode tempNode = findNodeByIndex(index);
        ListNode node = new ListNode(data);
        node.next = tempNode;
        node.prev = tempNode.prev;
        node.prev.next = node;
        node.next.prev = node;
    }
    // 找到插入位置
    private ListNode findNodeByIndex(int index) {
        ListNode current = head;
        while(index > 0){
            current = current.next;
            index--;
        }
        return current;
    }

    private void checkRangeForAdd(int index) {
        if (index < 0 || index > size()){
            throw new IndexOutOfBoundsException("下标不合法,index = " + index);
        }
    }

4.4删除第一次出现的关键字

public void remove(int key){
        if (head == null){
            return;
        }
        ListNode current = head;
        while (current != null){
            //找到目标值
            if(current.val == key){
            //判断是不是头节点head,不同位置操作不同
                if(current == head){
                    head = head.next;
                    if(head == null){
                 // 是头节点,并且链表为空
                        tail = null;
                    }else {
                 // 头节点并且链表不为空
                        head.prev.next = null;
                        head.prev = null;
                    }
                }else if (current == tail){
                  // 判断删除的是尾节点操作
                    tail = tail.prev;
                    tail.next = null;
                }else {
                  // 中间节点操作
                    current.next.prev = current.prev;
                    current.prev.next = current.next;
                }
                 // 操作完成退出
                return;
            }
               // 未完成继续遍历
            current = current.next;
        }
    }

4.5删除所有值为key的节点

此方法和删除第一次出现的关键字的方法极为相似,只是这个找到第一个关键字删除后并不会退出,接着遍历直至走完。要注意的点是:如果要删除的关键字是头节点并且只剩下一个节点,要记录一下head,不然会报错

                      // 只剩一个节点
                        head.prev.next = null;
                        current = head;
                        head.prev = null;

如下是实现代码: 

public void removeAllKey(int key){
        if (head == null) {
            return;
        }
        ListNode current = head;
        while (current != null) {
            // 头节点
            if (current.val == key) {
                if (current == head) {
                    head = head.next;
                    // 删除头节点
                    if (head == null) {
                        // 链表已经为空
                        tail = null;
                    } else {
                        // 只剩一个节点
                        head.prev.next = null;
                        current = head;
                        head.prev = null;
                    }
                } else if (current == tail) {
                    // 处理尾节点
                    tail = tail.prev;
                    tail.next = null;

                } else {
                // 中间节点操作
                    current.next.prev = current.prev;
                    current.prev.next = current.next;
                }
            }
            // 继续遍历
            current = current.next;
        }
    }

4.6其余方法

 剩下的方法就是和单链表实现相同,代码如下,不再过多解释

   public int size() {
        int count = 0;
        ListNode current = head;
        while(current != null){
            count++;
            current = current.next;
        }
        return count;
    }

    public void display() {
        StringBuilder sb = new StringBuilder();
        sb.append("[");
        ListNode current = head;
        while(current != null){
            sb.append(current.val);
            if (current.next != null){
                sb.append(",");
            }
            current = current.next;
        }
        sb.append("]");
        System.out.println(sb.toString());
    }

    public void clear(){
        head = null;
    }

在此我们已经介绍完双向链表。

5.单项链表和双向链表的区别

      在存储空间方面:单链表需要的存储空间比双向链表的要少,因为双向链表不仅保存有指向下一个节点的指针,还保存有指向上一个节点的指针,需要较大的空间来存储双向链表的指针域。
     在处理时间方面:双向链表的插入与删除操作比单链表的时间要快很多。在最末尾插入的时候,单链表需要找到需要插入位置的前一个节点,需要遍历整个链表,时间复杂度为O(n),而双向链表只需要head->tail,就可以得到最后一个节点的位置,然后就可以进行插入操作,时间复杂度为O(1)。在删除操作方面,单链表需要遍历到需要删除的节点的前一个节点,双向链表需要遍历到需要删除的节点,时间复杂度同为O(n)。


6.ArrayList 与 LinkedList区别

至此我们已经介绍完数据结构中arraylist 和 linkedlist 的相关知识。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值