LinkedList 与链表

LinkedList 与链表

链表

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

链表结构 :

  1. 单向或者双向
  2. 带头或者不带头
  3. 循环或者非循环

主要掌握两种 : 无头单向非循环链表: 结构简单 , 一般不会单独用来存数据 . 实际中更多是作为其他数据结构的子结构

​ 无头双向链表: 在Java的集合框架库中的 LinkedList 底层实现就是无头双向循环链表

链表实现

创建一个链表

无头单向非循环链表实现

static class ListNode {
    public int val;//存储的数据
    public ListNode next;//存储的下一个节点

    public ListNode(int val) {
        this.val = val;
    }
}
public ListNode head ;//代表当前链表的头结点的引用

public void creatList() {
    ListNode listNode1 = new ListNode(11);
    ListNode listNode2 = new ListNode(12);
    ListNode listNode3 = new ListNode(13);
    ListNode listNode4 = new ListNode(14);
    listNode1.next = listNode2;
    listNode2.next = listNode3;
    listNode3.next = listNode4;
    head = listNode1;
}

从指定位置开始打印链表

指定一个 cur 来表示 head , 防止 直接使用head遍历造成第二次遍历head找不到 , 使用 while 循环来遍历链表,当 cur 不为空时 , 将 cur .next 赋值给 cur

public void display() {
    //如果说 把整个链表遍历完成 那么就需要 head == null
    //如果说 要遍历到链表的尾巴 head.next = null;
    ListNode cur = head;
    //如果直接用 head 遍历 , 第二遍 遍历的时候会找不到head在哪里
    while (cur != null) {
        System.out.print(cur.val + " ");
        cur = cur.next;
    }
    System.out.println();
}

查找是否包含关键字key

指定一个 cur 来表示head , 代替head遍历链表 , 当cur 不为空时 , 看 key 的值 是否和cur.val 一致 , 如果不一致 , 将cur.next 赋值给 cur , 继续遍历 , 直到找到key 为止 , 结束循环

public boolean contains(int key) {
    ListNode cur = head;
    while (cur != null) {
        if (cur.val == key) {
            return true;
        }
        cur = cur.next;
    }
    return false;
}

得到单链表的长度

定义一个循环次数count , 指定cur 来表示 head来遍历链表 , 当 cur 不为空时 , count++ , 将 cur.next 的值给cur , 返回count

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

头插法

定义一个 node , 将头结点的地址 传给 node.next ,再将 node 的值 变为head

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

尾插法

如果 是个空链表 , 将node 的值 直接给head , 返回 . 如果非空 , 定义 cur 来代替head遍历数组 , 当 cue.next 不为null时 , 将 cur.next 赋值给 cur . 当cur 走到链表末尾时 , 即 cur.next = null , 再将node 插入

public void addLast(int data) {
    ListNode node = new ListNode(data);
    if (head == null) {
        head = node;
        return;
    }
    ListNode cur = head;
    while (cur.next != null) {
        cur = cur.next;
    }
    cur.next = node;
}

任意位置插入 , 第一个数据节点为0 下标

如果插入的位置 为头 , 直接调用 头插法

如果 插入位置 为链表的长度 , 调用尾插法

如果插入位置非头非尾 , 首先 , 检查插入位置是否合法 , 如果位置 小于 0 或者 大于链表长度 , 抛出位置不合法的异常 . 如果位置合法 , 找到想要插入位置的前一个位置下标 , 定义cur 来从头部遍历链表 , 定义循环次数 count , 当 count 不等于 index - 1 ,时 , 将 cur.next 给cur , count ++ , 直到 count = 位置减一 , 返回 cur

public void addIndex (int index,int data) throws ListIndexOutOfException{
    checkIndex(index);
    if (index == 0) {
        addFirst(data);
        return;
    }
    if (index == size()) {
        addLast(data);
        return;
    }
    ListNode cur = findIndexSubOne(index);
    ListNode node = new ListNode(data);
    node.next = cur.next;
    cur.next = node;
}
/**
 * 找到index - 1 的数据的位置
 */
private ListNode findIndexSubOne(int index) {
    ListNode cur = head;
    int count = 0;
    while (count != index - 1) {
        cur = cur.next;
        count++;
    }
    return cur;
}
private void checkIndex(int index) throws ListIndexOutOfException {
    if (index < 0 || index > size()) {
        throw new ListIndexOutOfException("index位置不合法");
    }
}

删除第一次出现的key 节点

如果链表为空 , 直接返回

如果要删除的值 等于 头结点的值, 直接让 head 往后走 head = head.next

如果删除的值不等于头结点的值 , 首先找到 要删除 Key 的前一个节点 , 定义 cur , 当cur.next 不为空时 , 如果 要删除的值 , 等于 cur.next.val , 返回 cur , 将cur .next 赋值给 del , 再将 del .next 赋值给 cur .next. 如果不等于 , cur 继续往后走

/**
 * 删除第一次出现key 节点
 */
public void remove(int key) {
    if (head == null) {
        return;
    }
    if (head.val == key) {
        head = head.next;
        return;
    }
    ListNode cur = searchPrev(key);
    if(cur == null) {
        return;
    }
    ListNode del = cur.next;//要删除的节点
    cur.next = del.next;

}
/**
 * 找到要删除 key的前一个节点
 */
private ListNode searchPrev(int key) {
    if(head == null) {
        return null;
    }
    ListNode cur = head;
    while (cur.next != null) {
        if(cur.next.val == key) {
            return cur;
        }
        cur = cur.next;
    }
    return null;//没有你要删除的节点
}

删除所有值为key 的节点

定义一个cur , 再定义一个cur的前驱 prev (小跟班) , 如果cur.val等于key时 , 将cur.next 赋值给 prev.next , cur继续向前遍历

(如果要删除12) 遇到是12 , 将cur.next 复制给 prev.next

如果cur.val 不等于key时 , 将cur赋值给prev ,cur继续遍历

public void removeAllKey(int key) {
    if(head == null) {
        return;
    }
    ListNode cur = head.next;
    ListNode prev = head;
    while (cur != null) {
        if (cur.val == key) {
            prev.next = cur.next;
            cur = cur.next;
        }else {
            prev = cur;
            cur = cur.next;
        }
    }
    if (head .val == key) {
        head = head.next;
    }
}

清空链表

直接将head置为null

public void clear() {
    head = null;
}

反转链表

如果 head 为空时 返回null . 如果只有一个节点 head.next 为null时 , 返回head

定义cur 为head.next . 将head.next 赋值为null(作为链表的尾部). 当cur不为空时 , 定义一个curNext为cur.next . 将每个节点头插法 插入链表 , 最后返回head

public ListNode reverseList() {
    if(head == null) {
        return null;
    }
    //说明只有一个节点
    if (head.next == null) {
        return head;
    }
    ListNode cur = head.next;
    head.next = null;

    while (cur != null) {
        ListNode curNext = cur.next;
        //头插法 插入cur
        cur.next = head;
        head = cur;
        cur = curNext;
    }
    return head;
}

找中间节点

找出并返回链表的中间节点 , 如果有两个中间节点 , 则返回第二个中间节点

定义快慢指针 , 快指针走两步, ,慢指针走一步 , 路程相同 , 所以当fast 在终点的时候 , slow 在中间位置

public ListNode middleNode() {
    ListNode fast = head;
    ListNode slow = head;
    while (fast != null && fast.next != null) {//顺序不能颠倒 , 否则空指针异常 .next的前提是 fast 不为空
        fast = fast.next.next;//走两步
        slow = slow.next;//走一步
    }
    return slow;
}

输出链表中倒数第k 个节点

定义快慢指针

  1. 先让fast 走 k-1 步
  2. fast 走到 k-1 步之后 , slow开始和 fast 一起走
  3. 当fast.next 为空时 , slow 所指的位置就是倒数第 k 个节点

比如 : 找倒数第三个节点 , fast 走 3 - 1 = 2 步 , 这是slow开始走 . 走到最后一个节点是 fast.next = null , slow 就是倒数第三个节点

public ListNode findKthToTail(int k) {
    if (k == 0 || head == null) {
        return null;
    }
    ListNode fast = head;
    ListNode slow = head;
    //1 .fast走 k-1步
    while (k-1 != 0) {
        fast = fast.next;
        if (fast == null) {
            return null;
        }
        k--;
    }
    //2 , 3
    while (fast.next != null) {
        fast = fast.next;
        slow = slow.next;
    }
    return slow;
}

合并两个有序链表 , 返回一个升序链表

定义一个虚拟节点newHead , 再定义一个tmp 来代表 newHead 遍历链表

​ 当head1 和 head2 都不为空时 , 如果 head1 < head2 的值. 将head1的值给tmp.next, 然后head1往后走 head1=head1.next. tmp也往后走
​ else 如果 head1 > head2的值, 将head2 的值给 tmp.next , head2往后走 head2 = head2.next. tmp 也往后走
​ 如果 head1不为空时 , tmp.next = head1
​ 如果 head2不为空时 , tmp.next = head2
最后返回 newHead.next

public static MySingleList.ListNode mergeTwoLists(MySingleList.ListNode head1, MySingleList.ListNode head2) {
    MySingleList.ListNode newHead = new MySingleList.ListNode(0);
    MySingleList.ListNode tmp = newHead;

    while (head1 != null && head2 != null) {
        if (head1.val < head2.val) {
            tmp.next = head1;
            head1 = head1.next;
            tmp = tmp.next;
        }else {
            tmp.next = head2;
            head2 = head2.next;
            tmp = tmp.next;
        }
    }
    if (head1 != null) {
        tmp.next = head1;
    }
    if (head2 != null) {
        tmp.next = head2;
    }
    return newHead.next;
}

链表的回文

如果 head 为空时 , 返回false . 如果链表中只有head一个节点 , 返回true.

定义一个快慢指针

1.找到链表中的中间节点

​ 快慢指针 当 fast不为空和fast.next不为空时 , 快指针走两步 , 慢指针走一步

2.将中间节点之后的节点反转

​ 定义 cur为 slow.next . 当cur 不为空时 , 定义curNext 为 cur.next . 将 cur.next 的值给 slow . slow往后走 slow = cur. cur也往后走 cur= cur.Next

3.头结点从前往后走 , 尾节点从后往前走

​ slow从后往前 , head从前往后 . 当slow.val = head.val 则有回文数

​ 偶数:如果head.next == slow 则返回true

​ 奇数:然后slow往前 slow=slow.next .head往后走 .head=head.next

​ 返回true

public boolean chkPalindrome(MySingleList.ListNode head) {
    if (head == null) {
        return false;
    }
    if (head.next == null) {
        return true;
    }
    MySingleList.ListNode fast = head;
    MySingleList.ListNode slow = head;
    //1. 找中间节点
    while (fast != null && fast.next != null) {
        fast = fast.next.next;
        slow = slow.next;
    }
    //2.反转
    MySingleList.ListNode cur = slow.next;
    while (cur != null) {
        MySingleList.ListNode curNext = cur.next;
        cur.next = slow;
        slow = cur;
        cur = curNext;
    }
    //3.从后往前走 , 从前往后走
    while (head != slow) {
        if (head.val != slow.val) {
            return false;
        }
        //偶数判断
        if (head.next == slow) {
            return true;
        }

        slow = slow.next;
        head = head.next;
    }
    return true;
}

LinkedList 的模拟实现

图示:无头双向链表

Linkedlist 需要定义尾节点 last

### 头插法

定义一个 node , 如果头结点 为空时 , 前驱和后继都是 node

不为空时 , 将 node 的后继给head , head 的前驱 给node , 再将head 给 node

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;
    }
}

尾插法

如果头结点为空 , 将 head 和 last 都给 node (直接插入)

或者 , 将last 的后继给node , node 的前驱给last , 再将node 变为 last

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;
    }
}

任意位置插入

1.当插入位置不合法时 , 抛出异常

2.当插入位置为头结点时 , 采用头插法 . 当插入位置为尾节点时 , 采用尾插法

3.定义 cur 表示要插入的位置 遍历链表 , 链表中间插入元素 , 需要更改四个位置

​ 1)将插入元素 node 的后继改为cur

​ 2)将cur 前一个节点的后继改为 node

​ 3)将 node 的前驱改为 cur 的前驱

​ 4)将 cur 的前驱改为 node

public void addIndex(int index,int data){
    if(index < 0 || index > size()) {
        throw new ListIndexOutOfException();
    }
    if(index == 0) {
        addFirst(data);
        return;
    }
    if(index == size()) {
        addLast(data);
        return;
    }

    ListNode cur = findIndex(index);
    //
    ListNode node = new ListNode(data);
    node.next = cur;
    cur.prev.next = node;
    node.prev = cur.prev;
    cur.prev = node;

}

查看是否包含关键字 key

​ 定义 cur 从 head 开始遍历链表 , 当 cur 不为空时 , 如果 cur 的值 等于 key , 返回 true , 否则 cur 继续遍历 , 直到找到为止 , 如果没有这可关键字 , 返回false

public boolean contains(int key){
    ListNode cur = head;

    while (cur != null) {
        if (cur.val == key) {
            return true;
        }
        cur = cur.next;
    }
    return false;
}

删除第一次出现的关键字为key 的节点

1.定义 cur 从头开始遍历

2.如果 cur 的值不等于要删除的关键字

​ 如果要删除的关键字为头结点 : 将 head 向后移一位 ,

​ 如果只有一个头结点 , 那就是 头结点不为空 , 将 head 的前驱置为空

​ 或者删除的是中间节点或者尾巴节点 : 将 cur 的前一个节点的后继置为 cur 的后继

​ 中间节点: 如果 cur的后继不为空 , 将 cur 后继的前驱置为 cur 的前驱

​ 尾巴节点: 将 last 置为last的前驱

表结构:

1.删除头结点

2)删除中间节点

​ 删除尾节点:

public void remove(int key){
    ListNode cur = head;
    while (cur != null) {
        if (cur.val == key) {
            //1.删除的是头结点
            if (cur == head) {
                head = head.next;
                if (head != null) {
                    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;
    }
}

删除所有值为 key 的节点

同上 , 删除上述代码的 return , 删完一次关键字后继续进行删除操作

public void removeAllKey(int key){
    ListNode cur = head;
    while (cur != null) {
        if (cur.val == key) {
            //1.删除的是头结点
            if (cur == head) {
                head = head.next;
                if (head != null) {
                    head.prev = null;
                }
            }else {
                //删除的是 中间节点或者尾巴节点
                cur.prev.next = cur.next;
                //中间节点
                if (cur.next != null) {
                    cur.next.prev = cur.prev;
                }else {
                    //尾巴节点
                }
            }
        }
        cur = cur.next;
    }
}

得到单链表的长度

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

置空链表

public void clear() {
    ListNode cur = head;
    while (cur != null) {
        ListNode curNext = cur.next;
        cur.next = null;
        cur.prev = null;
        cur = curNext;
    }
    head = null;
    last = null;
}

LinkedList 说明

​ LinkedList 的底层是双向链表 , 由于链表没有将元素存储在连续的空间中 , 元素存储在单独的节点中 , 然后通过引用将节点连接起来 , 因此在任意位置插入或者删除元素是 , 不需要搬移元素 , 效率较高

LinkedList 实现的接口

  • LinkedList 实现了 List 接口
  • LinkedList 的底层使用了双向链表
  • LinkedList 没有实现RandomAccess 接口 , 因此LinkedList 不支持随机访问
  • LinkedList 的任意位置插入和删除元素时效率比较高 , 时间复杂度为 O(1)
  • LinkedList 比较适合任意位置插入的场景

LinkedList 的构造

public static void main(String[] args) {
    // 构造一个空的LinkedList
    List<Integer> list1 = new LinkedList<>();
    List<String> list2 = new java.util.ArrayList<>();
    list2.add("JavaSE");
    list2.add("JavaWeb");
    list2.add("JavaEE");
    // 使用ArrayList构造LinkedList
    List<String> list3 = new LinkedList<>(list2);
}

ArrayList 和 LinkedList 的区别

不同ArrayListLinkedList
存储空间上物理上一定连续逻辑上连续, 但物理上不一定连续
随机访问支持 O(1)不支持 😮(n)
头插需要搬移元素 , 效率低O(n)只需要修改引用的指向 , 时间复杂度为O(1)
插入空间不够时需要扩容没有容量的概念
应用场景元素高效存储 + 频繁访问任意位置插入和删除频繁




























33


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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值