链表(Linked List)

  • 链表的接口设计
  • 单向链表(SingleLinkedList)
    • 获取元素 – get()
    • 清空元素 – clear()
    • 添加元素 – add(int index, E element)
    • 删除元素 – remove(int index)
    • 单向链表完整源码
  • 带虚拟头结点的单向链表
  • 动态数组、链表复杂度分析
  • 双向链表(LinkedList)
    • 双向链表 – get(int index)
    • 双向链表 – add(int index, E element)
    • 双向链表 – remove(int index)
    • 双向链表完整源码
  • 双向链表 vs 单向链表
  • 双向链表 vs 动态数组
  • 练习题
    • 练习 – 删除链表中的节点
    • 练习 – 反转一个链表(递归、非递归解法)
    • 练习 – 判断一个链表是否有环(快慢指针)

数据结构与算法笔记目录:《恋上数据结构》 笔记目录

想加深 Java 基础推荐看这个: Java 强化笔记目录

动态数组有个明显的缺点:

  • 可能会造成内存空间的大量浪费。

能否用到多少就申请多少内存?

  • 链表可以办到这一点

链表是一种链式存储的线性表,所有元素的内存地址不一定是连续的;

链表的接口设计


由于链表的大部分接口和动态数组一致,我们抽取出一个共同的 List 接口

package com.mj;public interface List<E> {static final int ELEMENT_NOT_FOUND = -1;/*** 清除所有元素*/void clear();/*** 元素的数量* @return*/int size();/*** 是否为空* @return*/boolean isEmpty();/*** 是否包含某个元素* @param element* @return*/boolean contains(E element);/*** 添加元素到尾部* @param element*/void add(E element);/*** 获取index位置的元素* @param index* @return*/E get(int index);/*** 设置index位置的元素* @param index* @param element* @return 原来的元素ֵ*/E set(int index, E element);/*** 在index位置插入一个元素* @param index* @param element*/void add(int index, E element);/*** 删除index位置的元素* @param index* @return*/E remove(int index);/*** 查看元素的索引* @param element* @return*/int indexOf(E element);
}

再将一些通用的字段与方法放到一个抽象类中,无论是动态数组还是链表只需要继承这个抽象类即可。

package com.mj;public abstract class AbstractList<E> implements List<E>{protected int size;// 下标越界抛出的异常protected void outOfBounds(int index) {throw new IndexOutOfBoundsException("Index:" + index + ", Size:" + size);}// 检查下标越界(不可访问或删除size位置)protected void rangeCheck(int index){if(index < 0 || index >= size){outOfBounds(index);}}// 检查add()的下标越界(可以在size位置添加元素)protected void rangeCheckForAdd(int index) {if (index < 0 || index > size) {outOfBounds(index);}}@Overridepublic boolean contains(E element) {return indexOf(element)!=ELEMENT_NOT_FOUND;}@Overridepublic int size() {return size;}@Overridepublic boolean isEmpty() {return size == 0;}@Overridepublic void add(E element) {add(size, element);}}

单向链表(SingleLinkedList)

单向链表的结构如下图所示:

public class SingleLinkedList<E> extends AbstractList<E> {private Node<E> first;// 链表中的节点private static class Node<E> {E element; // 节点元素Node<E> next; // 节点指向下一个节点public Node(E element, Node<E> next) {this.element = element;this.next = next;}}}

获取元素 – get()

@Override
public E get(int index) {return node(index).element;
}
/*** 根据索引找到节点*/
private Node<E> node(int index) {rangeCheck(index);Node<E> node = first;for (int i = 0; i < index; i++) {node = node.next;}return node;
}

清空元素 – clear()

  • next 不需要设置为 null,因为 first 指向了 null,后面的 Node 没有被指向,在 Java 中会自动被垃圾回收。
@Override
public void clear() {size = 0;first = null;
}

添加元素 – add(int index, E element)


添加元素尤其要注意 0 位置,给空链表添加第一个节点是个特殊情况:

@Override
public void add(int index, E element) {/** 最好:O(1)* 最坏:O(n)* 平均:O(n)*/rangeCheckForAdd(index);if(index == 0){ // 给空链表添加第一个元素的情况first = new Node<>(element, first);}else{Node<E> prev = node(index - 1);prev.next = new Node<>(element, prev.next);}size++;
}

删除元素 – remove(int index)

@Override
public E remove(int index) {/** 最好:O(1)* 最坏:O(n)* 平均:O(n)*/rangeCheck(index);Node<E> node = first;if (index == 0) { // 删除第一个元素是特殊情况first = first.next;} else {Node<E> prev = node(index - 1); // 找到前一个元素node = prev.next; // 要删除的元素prev.next = node.next; // 删除元素}size--;return node.element;
}

单向链表完整源码

package com.mj.single;import com.mj.AbstractList;/*** 单向链表* @author yusael*/
public class SingleLinkedList<E> extends AbstractList<E> {private Node<E> first;// 链表中的节点private static class Node<E> {E element; // 节点元素Node<E> next; // 节点指向下一个节点public Node(E element, Node<E> next) {this.element = element;this.next = next;}}/*** 根据索引找到节点对象*/private Node<E> node(int index) {rangeCheck(index);Node<E> node = first;for (int i = 0; i < index; i++) {node = node.next;}return node;}@Overridepublic void clear() {size = 0;first = null;}@Overridepublic E get(int index) {/** 最好:O(1)* 最坏:O(n)* 平均:O(n)*/return node(index).element;}@Overridepublic E set(int index, E element) {/** 最好:O(1)* 最坏:O(n)* 平均:O(n)*/E old = node(index).element;node(index).element = element;return old;}@Overridepublic void add(int index, E element) {/** 最好:O(1)* 最坏:O(n)* 平均:O(n)*/rangeCheckForAdd(index);if (index == 0) { // 给空链表添加第一个元素的情况first = new Node<>(element, first);} else {Node<E> prev = node(index - 1);prev.next = new Node<>(element, prev.next);}size++;}@Overridepublic E remove(int index) {/** 最好:O(1)* 最坏:O(n)* 平均:O(n)*/rangeCheck(index);Node<E> node = first;if (index == 0) { // 删除第一个元素是特殊情况first = first.next;} else {Node<E> prev = node(index - 1); // 找到前一个元素node = prev.next; // 要删除的元素prev.next = node.next; // 删除元素}size--;return node.element;}@Overridepublic int indexOf(E element) {// 有个注意点, 如果传入元素为null, 则不能调用equals方法, 否则会空指针// 因此需要对元素是否为null做分别处理if (element == null) {Node<E> node = first;for (int i = 0; i < size; i++) {if (node.element == null) return i;node = node.next;}} else {Node<E> node = first;for (int i = 0; i < size; i++) {if (node.element.equals(element)) return i;node = node.next;}}return ELEMENT_NOT_FOUND;}@Overridepublic String toString() {StringBuilder string = new StringBuilder();string.append("[size=").append(size).append(", ");Node<E> node = first;for (int i = 0; i < size; i++) {if (i != 0) {string.append(", ");}string.append(node.element);node = node.next;}string.append("]");return string.toString();}}

带虚拟头结点的单向链表

有时候为了让代码更加精简,统一所有节点的处理逻辑,可以在最前面增加一个虚拟的头结点(不存储数据)

带虚拟头结点的单向链表与普通单向链表代码类似:但是 addreomove 略有不同;

package com.mj.single;import com.mj.AbstractList;/*** 增加一个虚拟头结点* @author yusael*/
public class SingleLinkedList2<E> extends AbstractList<E> {private Node<E> first;//**********************************public SingleLinkedList2() { // 初始化一个虚拟头结点first = new Node<>(null, null);};//**********************************private static class Node<E> {E element;Node<E> next;public Node(E element, Node<E> next) {this.element = element;this.next = next;}}@Overridepublic void clear() {size = 0;first = null;}@Overridepublic E get(int index) {return node(index).element;}@Overridepublic E set(int index, E element) {E old = node(index).element;node(index).element = element;return old;}@Overridepublic void add(int index, E element) {rangeCheckForAdd(index);Node<E> prev = (index == 0) ? first : node(index - 1);prev.next = new Node<>(element, prev.next);size++;}@Overridepublic E remove(int index) {rangeCheck(index);Node<E> prev = (index == 0) ? first : node(index - 1);Node<E> node = prev.next;prev.next = node.next;size--;return prev.element;}@Overridepublic int indexOf(E element) {// 有个注意点, 如果传入元素为null, 则不能调用equals方法, 否则会空指针// 因此需要对元素是否为null做分别处理if (element == null) {Node<E> node = first;for (int i = 0; i < size; i++) {if (node.element == null) return i;node = node.next;}} else {Node<E> node = first;for (int i = 0; i < size; i++) {if (node.element.equals(element)) return i;node = node.next;}}return ELEMENT_NOT_FOUND;}/*** 根据索引找到节点* * @param index* @return*/private Node<E> node(int index) {rangeCheck(index);Node<E> node = first.next;for (int i = 0; i < index; i++) {node = node.next;}return node;}@Overridepublic String toString() {StringBuilder string = new StringBuilder();string.append("[size=").append(size).append(", ");Node<E> node = first.next;for (int i = 0; i < size; i++) {if (i != 0) {string.append(", ");}string.append(node.element);node = node.next;}string.append("]");return string.toString();}}

动态数组、链表复杂度分析

数组的随机访问速度非常快:elements[n] 的效率与 n 是多少无关;

双向链表(LinkedList)

双向链表可以提升链表的综合性能;

双向链表只有一个元素的情况:firstlast 指向同一个节点

/*** 双向链表* @author yusael*/
public class LinkedList<E> extends AbstractList<E> {private Node<E> first;private Node<E> last;private static class Node<E> {E element;Node<E> prev;Node<E> next;public Node(Node<E> prev, E element, Node<E> next) {this.prev = prev;this.element = element;this.next = next;}@Overridepublic String toString(){StringBuilder sb = new StringBuilder();if(prev != null){sb.append(prev.element);}else{sb.append("null");}sb.append("_").append(element).append("_");if(next != null){sb.append(next.element);}else{sb.append("null");}return sb.toString();}}
}

双向链表 – get(int index)

@Override
public E get(int index) {return node(index).element;
}
/*** 根据索引找到节点*/
private Node<E> node(int index) {rangeCheck(index);if (index < (size >> 1)) { // 索引小于一半从前往后找Node<E> node = first;for (int i = 0; i < index; i++) {node = node.next;}return node;} else { // 索引大于一半从后往前找Node<E> node = last;for (int i = size - 1; i > index; i--) {node = node.prev;}return node;}
}

双向链表 – add(int index, E element)

@Override
public void add(int index, E element) {rangeCheckForAdd(index);// size == 0// index == 0if (index == size) { // 往最后面添加元素Node<E> oldLast = last;last = new Node<>(oldLast, element, null);if (oldLast == null) { // 这是链表添加的第一个元素first = last;} else {oldLast.next = last;}} else { // 正常添加元素Node<E> next = node(index);Node<E> prev = next.prev;Node<E> node = new Node<>(prev, element, next);next.prev = node;if (prev == null) { // index == 0first = node;} else {prev.next = node;}}size++;
}

双向链表 – remove(int index)

@Override
public E remove(int index) {rangeCheck(index);Node<E> node = node(index);Node<E> prev = node.prev;Node<E> next = node.next;if (prev == null) { // index == 0first = next;} else {prev.next = next;}if (next == null) { // index == size - 1last = prev;} else {next.prev = prev;}size--;return node.element;
}

双向链表完整源码

package com.mj;import com.mj.AbstractList;/*** 双向链表* @author yusael*/
public class LinkedList<E> extends AbstractList<E> {private Node<E> first;private Node<E> last;private static class Node<E> {E element;Node<E> prev; // 指向前驱节点Node<E> next; // 指向后继节点public Node(Node<E> prev, E element, Node<E> next) {this.prev = prev;this.element = element;this.next = next;}@Overridepublic String toString(){StringBuilder sb = new StringBuilder();if(prev != null){sb.append(prev.element);}else{sb.append("null");}sb.append("_").append(element).append("_");if(next != null){sb.append(next.element);}else{sb.append("null");}return sb.toString();}}@Overridepublic void clear() {size = 0;first = null;last = null;}@Overridepublic E get(int index) {return node(index).element;}@Overridepublic E set(int index, E element) {Node<E> node = node(index);E old = node.element;node.element = element;return old;}@Overridepublic void add(int index, E element) {rangeCheckForAdd(index);// size == 0// index == 0if (index == size) { // 往最后面添加元素Node<E> oldLast = last;last = new Node<>(oldLast, element, null);if (oldLast == null) { // 这是链表添加的第一个元素first = last;} else {oldLast.next = last;}} else { // 正常添加元素Node<E> next = node(index);Node<E> prev = next.prev;Node<E> node = new Node<>(prev, element, next);next.prev = node;if (prev == null) { // index == 0first = node;} else {prev.next = node;}}size++;}@Overridepublic E remove(int index) {rangeCheck(index);Node<E> node = node(index);Node<E> prev = node.prev;Node<E> next = node.next;if (prev == null) { // index == 0first = next;} else {prev.next = next;}if (next == null) { // index == size - 1last = prev;} else {next.prev = prev;}size--;return node.element;}@Overridepublic int indexOf(E element) {if (element == null) {Node<E> node = first;for (int i = 0; i < size; i++) {if (node.element == element) return i;node = node.next;}} else {Node<E> node = first;for (int i = 0; i < size; i++) {if (node.element.equals(element)) return i;node = node.next;}}return ELEMENT_NOT_FOUND;}/*** 根据索引找到节点* * @param index* @return*/private Node<E> node(int index) {rangeCheck(index);if (index < (size >> 1)) { // 索引小于一半从前往后找Node<E> node = first;for (int i = 0; i < index; i++) {node = node.next;}return node;} else { // 索引大于一半从后往前找Node<E> node = last;for (int i = size - 1; i > index; i--) {node = node.prev;}return node;}}@Overridepublic String toString() {StringBuilder string = new StringBuilder();string.append("[size=").append(size).append(", ");Node<E> node = first;for (int i = 0; i < size; i++) {if (i != 0) {string.append(", ");}string.append(node);node = node.next;}string.append("]");return string.toString();}}

双向链表 vs 单向链表

粗略对比一下删除的操作数量:操作数量缩减了近一半

有了双向链表,单向链表是否就没有任何用处了?

  • 并非如此,在哈希表的设计中就用到了单链表
  • 至于原因,在哈希表章节中会讲到

双向链表 vs 动态数组

动态数组:开辟、销毁内存空间的次数相对较少,但可能造成内存空间浪费(可以通过缩容解决)
双向链表:开辟、销毁内存空间的次数相对较多,但不会造成内存空间的浪费

  • 如果频繁在尾部进行添加、删除操作,动态数组、双向链表均可选择
  • 如果频繁在头部进行添加、删除操作,建议选择使用双向链表
  • 如果有频繁的 (在任意位置)添加、删除操作,建议选择使用双向链表
  • 如果有频繁的查询操作(随机访问操作),建议选择使用动态数组

练习题

练习 – 删除链表中的节点

237_删除链表中的节点:https://leetcode-cn.com/problems/delete-node-in-a-linked-list/


/*** Definition for singly-linked list.* public class ListNode {*     int val;*     ListNode next;*     ListNode(int x) { val = x; }* }*/
class Solution {public void deleteNode(ListNode node) {node.val = node.next.val;node.next = node.next.next;}
}

练习 – 反转一个链表(递归、非递归解法)

206_反转链表:https://leetcode-cn.com/problems/reverse-linked-list/

递归解法


/*** Definition for singly-linked list.* public class ListNode {*     int val;*     ListNode next;*     ListNode(int x) { val = x; }* }*/
class Solution {public ListNode reverseList(ListNode head) {if(head == null) return null; // 空链表if(head.next == null) return head; // 只有一个节点ListNode newHead = reverseList(head.next);head.next.next = head; // newHead->1->2->3->4->5->nullhead.next = null;return newHead;}
}

非递归解法 - 头插法:

/*** Definition for singly-linked list.* public class ListNode {*     int val;*     ListNode next;*     ListNode(int x) { val = x; }* }*/
class Solution {public ListNode reverseList(ListNode head) {ListNode newHead = null;while (head != null) {ListNode tmp = head.next;head.next = newHead;newHead = head;head = tmp;}return newHead;}
}

练习 – 判断一个链表是否有环(快慢指针)

141_环形链表:https://leetcode-cn.com/problems/linked-list-cycle/



快慢指针解法:

/*** Definition for singly-linked list.* class ListNode {*     int val;*     ListNode next;*     ListNode(int x) {*         val = x;*         next = null;*     }* }*/
public class Solution {public boolean hasCycle(ListNode head) {if(head == null || head.next == null) return false;ListNode slow = head;ListNode fast = head.next; // 快指针每次都比慢指针快一步(包括开始)while (fast != null && fast.next != null) {if (slow.val == fast.val) return true;slow = slow.next;fast = fast.next.next;}return false;}
}

《恋上数据结构第1季》单向链表、双向链表相关推荐

  1. 《恋上数据结构第1季》动态数组实现栈

    栈(Stack) 栈的应用 – 浏览器的前进和后退 栈的接口设计 动态数组实现栈 练习题 逆波兰表达式求值 有效的括号 数据结构与算法笔记目录:<恋上数据结构> 笔记目录 想加深 Java ...

  2. 《恋上数据结构第1季》二叉搜索树BST

    二叉搜索树(BinarySearchTree) BST 接口设计 BST 基础 添加元素: add() 删除元素: remove() 删除节点 – 叶子节点 删除节点 – 度为1的节点 删除节点 – ...

  3. 《恋上数据结构第1季》单向循环链表、双向循环链表以及约瑟夫环问题

    循环链表(CircleList) 链表的接口设计 单向循环链表 单向循环链表完整源码 双向循环链表 双向循环链表完整源码 双向循环链表解决约瑟夫环问题 如何发挥循环链表的最大威力? 静态链表 数据结构 ...

  4. 《恋上数据结构第1季》哈希表介绍以及从源码分析哈希值计算

    哈希表(Hash Table) 引出哈希表 哈希表(Hash Table) 哈希冲突(Hash Collision) JDK1.8的哈希冲突解决方案 哈希函数 如何生成 key 的哈希值 Intege ...

  5. 《恋上数据结构第1季》映射 TreeMap,HashMap,LinkedHashMap

    映射 Map Map的接口定义 Map.java 红黑树 RBTree 实现 TreeMap TreeMap 分析 哈希表实现 HashMap HashMap 升级为 LinkedHashMap 数据 ...

  6. 《恋上数据结构第1季》集合 ListSet、TreeSet、HashSet

    集合(Set) 集合的接口定义 双向链表 LinkedList 实现 ListSet 红黑树 RBTree 实现 TreeSet TreeMap 实现 TreeSet HashMap 实现 HashS ...

  7. 《恋上数据结构第1季》平衡二叉搜索树、AVL树

    AVL树 二叉搜索树缺点分析 改进二叉搜索树 平衡(Balance) 理想平衡 如何改进二叉搜索树? 平衡二叉搜索树(Balanced Binary Search Tree) AVL树 BST 对比 ...

  8. 《恋上数据结构第1季》队列、双端队列、循环队列、循环双端队列

    队列(Queue) 队列 Queue 队列的接口设计 队列源码 双端队列 Deque 双端队列接口设计 双端队列源码 循环队列 Circle Queue 循环队列实现 索引映射封装 循环队列 – %运 ...

  9. 《恋上数据结构第1季》动态扩容数组原理及实现

    动态扩容数组 什么是数据结构? 线性表 数组(Array) 动态数组(Dynamic Array) 动态数组接口设计 清除所有元素 - clear() 添加元素 - add(E element).ad ...

最新文章

  1. Python使用过滤器(filter)进行图像模糊处理
  2. SAP RETAIL 商品主数据里影响自动补货结果的几个参数 I
  3. nodejs返回下载文档,文档名称出现汉字出现乱码解决
  4. Intel Realsense D435 连续验证 摄像头初始化 hardware_reset() 失败案例
  5. 6-2 链式表的按序号查找
  6. 数独游戏技巧从入门到精通_如何引导孩子入门九宫格数独?掌握4个技巧口诀,孩子思维提升快...
  7. Yarn 和 Npm 命令行切换 摘录
  8. ajax success重复,ajax中success函数中的事件会叠加吗?
  9. linux监听报错sp2-0734,Linux中Oracle启动侦听报错TNS:permission denied的解决方法
  10. android:windowSoftInputMode属性;界面关闭后软键盘不隐藏的解决方法;
  11. Spring Data JPA 概述 与 快速入门(操作 mysql 数据)
  12. java教学视频_孔浩老师_孔浩Java教学系列视频教程 - 轻松自学网
  13. c#明华rf读卡器_RF通用开发包 明华RF读卡器 demo for c#(RF reader demo for c#) - 下载 - 搜珍网...
  14. java架构师有哪些证书,22年最新
  15. android 模板引擎,template.js模板引擎
  16. OA系统权限管理设计方案
  17. 服务认证的介绍-实施依据及作用
  18. java中的steam流
  19. 每日分享正能量一段话45句
  20. Cypher高级查询

热门文章

  1. 35岁真的是职场分水岭吗?
  2. RISC-V架构能否有效挑战ARM和英特尔?
  3. 谈一谈不常见却又不可少的ThreadLocal
  4. 你觉得一个128g主力机用几年会到非换不可的程度?
  5. sql初学者指南_初学者SQL示例:SQL SELECT语句的用法
  6. Mac OS 上配置java开发环境
  7. EasyNVR智能云终端硬件与EasyNVR解决方案软件综合对比
  8. JavaScript函数声明提升
  9. 软件工程结对作业01
  10. Java之正則表達式【使用语法】