《恋上数据结构第1季》单向链表、双向链表
链表(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();}}
带虚拟头结点的单向链表
有时候为了让代码更加精简,统一所有节点的处理逻辑,可以在最前面增加一个虚拟的头结点(不存储数据)。
带虚拟头结点的单向链表与普通单向链表代码类似:但是 add
、reomove
略有不同;
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)
双向链表可以提升链表的综合性能;
双向链表只有一个元素的情况:first
、last
指向同一个节点
/*** 双向链表* @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季》动态数组实现栈
栈(Stack) 栈的应用 – 浏览器的前进和后退 栈的接口设计 动态数组实现栈 练习题 逆波兰表达式求值 有效的括号 数据结构与算法笔记目录:<恋上数据结构> 笔记目录 想加深 Java ...
- 《恋上数据结构第1季》二叉搜索树BST
二叉搜索树(BinarySearchTree) BST 接口设计 BST 基础 添加元素: add() 删除元素: remove() 删除节点 – 叶子节点 删除节点 – 度为1的节点 删除节点 – ...
- 《恋上数据结构第1季》单向循环链表、双向循环链表以及约瑟夫环问题
循环链表(CircleList) 链表的接口设计 单向循环链表 单向循环链表完整源码 双向循环链表 双向循环链表完整源码 双向循环链表解决约瑟夫环问题 如何发挥循环链表的最大威力? 静态链表 数据结构 ...
- 《恋上数据结构第1季》哈希表介绍以及从源码分析哈希值计算
哈希表(Hash Table) 引出哈希表 哈希表(Hash Table) 哈希冲突(Hash Collision) JDK1.8的哈希冲突解决方案 哈希函数 如何生成 key 的哈希值 Intege ...
- 《恋上数据结构第1季》映射 TreeMap,HashMap,LinkedHashMap
映射 Map Map的接口定义 Map.java 红黑树 RBTree 实现 TreeMap TreeMap 分析 哈希表实现 HashMap HashMap 升级为 LinkedHashMap 数据 ...
- 《恋上数据结构第1季》集合 ListSet、TreeSet、HashSet
集合(Set) 集合的接口定义 双向链表 LinkedList 实现 ListSet 红黑树 RBTree 实现 TreeSet TreeMap 实现 TreeSet HashMap 实现 HashS ...
- 《恋上数据结构第1季》平衡二叉搜索树、AVL树
AVL树 二叉搜索树缺点分析 改进二叉搜索树 平衡(Balance) 理想平衡 如何改进二叉搜索树? 平衡二叉搜索树(Balanced Binary Search Tree) AVL树 BST 对比 ...
- 《恋上数据结构第1季》队列、双端队列、循环队列、循环双端队列
队列(Queue) 队列 Queue 队列的接口设计 队列源码 双端队列 Deque 双端队列接口设计 双端队列源码 循环队列 Circle Queue 循环队列实现 索引映射封装 循环队列 – %运 ...
- 《恋上数据结构第1季》动态扩容数组原理及实现
动态扩容数组 什么是数据结构? 线性表 数组(Array) 动态数组(Dynamic Array) 动态数组接口设计 清除所有元素 - clear() 添加元素 - add(E element).ad ...
最新文章
- Python使用过滤器(filter)进行图像模糊处理
- SAP RETAIL 商品主数据里影响自动补货结果的几个参数 I
- nodejs返回下载文档,文档名称出现汉字出现乱码解决
- Intel Realsense D435 连续验证 摄像头初始化 hardware_reset() 失败案例
- 6-2 链式表的按序号查找
- 数独游戏技巧从入门到精通_如何引导孩子入门九宫格数独?掌握4个技巧口诀,孩子思维提升快...
- Yarn 和 Npm 命令行切换 摘录
- ajax success重复,ajax中success函数中的事件会叠加吗?
- linux监听报错sp2-0734,Linux中Oracle启动侦听报错TNS:permission denied的解决方法
- android:windowSoftInputMode属性;界面关闭后软键盘不隐藏的解决方法;
- Spring Data JPA 概述 与 快速入门(操作 mysql 数据)
- java教学视频_孔浩老师_孔浩Java教学系列视频教程 - 轻松自学网
- c#明华rf读卡器_RF通用开发包 明华RF读卡器 demo for c#(RF reader demo for c#) - 下载 - 搜珍网...
- java架构师有哪些证书,22年最新
- android 模板引擎,template.js模板引擎
- OA系统权限管理设计方案
- 服务认证的介绍-实施依据及作用
- java中的steam流
- 每日分享正能量一段话45句
- Cypher高级查询