💕"你笑的次数越多越好,因为你只有用笑才能不怀恶意地消灭罪恶。"💕
作者:Mylvzi
文章主要内容:数据结构之单链表的模拟实现
在上个顺序表的博客结尾针对ArrayList的缺陷留了一个思考题
![]()
大家可以去再看一下顺序表这篇博客https://blog.csdn.net/Mylvzi/article/details/133896470?spm=1001.2014.3001.5501
因为ArrayList的底层是一段连续内存的空间,进行插入/删除操作的开销很大,因此ArrayList不适合用于"需要大量添加/删除数据的场景",因此Java中又引入了LinkedList,即链表结构
链表是物理地址不连续的线性表,逻辑顺序是通过"链条"连接起来的
可以将链表想象成拉着很多节车厢的火车


注意:
1.链表是由一个个结点(Node)组成的,每个结点都是从堆上申请的
2.堆为每个结点分配内存地址,分配时是随机的,所以链表的物理结构是不连续的
链表的分类:
按照不同的性质可以有多种分类方法:



综上,链表可分为2 * 2 * 2 = 8种,在这里我们只需重点掌握两种链表即可
注意:这里说的"有无头节点"中的头节点不属于链表中的有效数据,他更像是一个“傀儡节点”,仅仅作为前驱使用

- // 定义结点
- static class ListNode {
- int val;// 结点存储的数据
- ListNode next;// 保存下一个结点的地址
-
- public ListNode(int val) {
- this.val = val;
- }
- }
-
- // 定义头节点
- public ListNode head = null;
因为链表的物理地址并不是连续的,所以我们插入数据的位置是多样性的,总的来说可以分为三种
头插法及创建一个新的结点,将此结点插入到第一个结点之前

- public void addFirst(int data) {
- ListNode newNode = new ListNode(data);
-
- // 头插法不需要考虑链表是否为空
- newNode.next = head;
- head = newNode;
- }
注意:
在链表中插入/删除数据时,最重要的一点是记得保存"下一节点",往往是先连接后面的结点,在进行其他的操作(如果不先连接后面的结点就会损失后面的所有节点火车一节车厢断开,则后面的车厢都断开了)
尾插法即在链表的末尾添加一个新的结点

- public void addLast(int data) {
- ListNode newNode = new ListNode(data);
-
- // 如果为空 链表中没有一个节点 直接插入
- if(head == null) {
- head = newNode;
- return;
- }
-
- ListNode cur = head;
-
- // 让cur走到链表的最后一个节点
- while (cur.next != null) {
- cur = cur.next;
- }
-
- cur.next = newNode;
- }
给指定的位置index,在指定位置插入一个节点(第一节点对应的index为0)

- public void addIndex(int index,int data) {
- // 先检查index是否合法
- if(index < 0 || index > size()) {
- throw new posOutOfBoundException(index + "位置不合法");
- }
-
- if (index == 0) addFirst(data);
- if (index == size()) addLast(data);
-
- // 让cur走到要删除结点的前驱节点 走index - 1步
- ListNode cur = head;
- for (int i = 0; i < index-1; i++) {
- cur = cur.next;
- }
-
- ListNode newNode = new ListNode(data);
- newNode.next = cur.next;
- cur.next = newNode;
- }
- // 判断是否包含
- public boolean contains(int key) {
- ListNode cur = head;
- while (cur != null) {
- if (cur.val == key) {
- return true;
- }
-
- cur = cur.next;
- }
-
- return false;
- }
- // 返回长度
- public int size() {
- ListNode cur = head;
- int cnt = 0;// 设置计数器
-
- while (cur != null) {
- cnt++;
- cur = cur.next;
- }
-
- return cnt;
- }
要进行此删除操作,关键还是要在删除对应节点之后仍然连接之后的结点

- public void remove(int key) {
- if(head == null) {
- throw new RuntimeException("链表为空 无法删除");
- }
-
- if (head.val == key) {
- head = head.next;
- return;
- }
-
- // 让cur走到要删除节点的前驱节点
- ListNode cur = findPrevNode(key);
- if(cur == null) {
- System.out.println("没有你要删除的结点");
- return;
- }
-
- cur.next = cur.next.next;
- }
-
- // 找要删除结点的前驱结点
- private ListNode findPrevNode(int key) {
- ListNode cur = head;
- while (cur != null) {
- if (cur.next.val == key) {
- return cur;
- }
- cur = cur.next;
- }
-
- return null;
- }

- public void removeAll(int key) {
- if(head == null) {
- throw new RuntimeException("链表为空 无法删除");
- }
-
- // 处理头节点是key的情况
- while (head.val == key) {
- head = head.next;
- }
-
- // 设置两个结点 前驱节点prev 和用于遍历的结点cur
- ListNode prev = head;
- ListNode cur = head.next;
-
- while (cur != null) {
-
- // 等于 进行删除
- if (cur.val == key) {
- prev.next = cur.next;
- cur = cur.next;
-
- continue;
- }
-
- prev = prev.next;
- cur = cur.next;
- }
-
- }
注意考虑当头节点是要删除的结点
- // 打印链表
- public void display() {
- // 要始终保持head结点是头节点 此处使用一个临时结点cur用于遍历整个链表
- ListNode cur = head;
- while (cur != null) {
- System.out.print(cur.val);
- cur = cur.next;
- }
- }
LinkedList是Java中使用双向链表实现的一中数据结构,每个节点都含有三个属性:

在集合框架中,LinkedList也实现了List接口

说明:
1.LinkedList没有实现RandomAccess接口,因此LinkedList不支持随机访问
2.LinkedList适合用于大量进行数据增加/删除的操作
LinkedList是一个双向的链表,且有两个"标记结点"head,last,分别用于标记链表的头/尾

- public class MyLinkedList {
-
- static class ListNode {
- private int data;
- private ListNode prev;
- private ListNode next;
-
- public ListNode(int data) {
- this.data = data;
- }
- }
-
- public ListNode head;
- public ListNode last;
-
- /**
- * 前三个方法和prev没关系
- * 只需遍历整个链表即可
- * @return
- */
-
- //得到单链表的长度
- public int size(){
- int cnt = 0;
- ListNode cur = head;
- while (cur != null) {
- cnt++;
- cur = cur.next;
- }
-
- return cnt;
- }
-
- // 打印链表
- public void display(){
- ListNode cur = head;
- while (cur != null) {
- System.out.print(cur.data + " ");
- cur = cur.next;
- }
- System.out.println();
- }
-
- //查找是否包含关键字key是否在单链表当中
- public boolean contains(int key){
- ListNode cur = head;
- while (cur != null) {
- if (cur.data == key) {
- return true;
- }
- cur = cur.next;
- }
-
- System.out.println("链表不包含该元素");
- return false;
- }
-
-
- //头插法
- public void addFirst(int data){
- ListNode node = new ListNode(data);
-
- // 为空,直接赋值
- if (head == null) {
- head = last = node;
- }else {
- node.next = head;
- head.prev = node;
- head = node;
- }
-
- }
-
- //尾插法 最方便的一集 因为本来就有尾结点
- public void addLast(int data){
- ListNode node = new ListNode(data);
-
- // 为空,直接赋值
- if (head == null) {
- head = last = node;
- }else {
- last.next = node;
- node.prev = last;
- last = node;
- }
-
-
- }
- //任意位置插入,第一个数据节点为0号下标
- public void addIndex(int index,int data){
- if(index<0 || index>size()) {
- throw new RuntimeException(index + "位置异常");
- }
-
- if (index == 0) {
- addFirst(data);
- return;
- }
- if(index == size()){
- addLast(data);
- return;
- }
-
- ListNode cur = searchIndex(index);
- ListNode node = new ListNode(data);
-
-
- // 插入 对于单链表来说需要cur走到Index位置的前一个结点 而双向链表不需要 直接走到index位置的结点即可
- // 在index位置的插入都是先绑定后面的
- node.next = cur;
- cur.prev.next = node;
-
- // 这两行的顺序不能改变
- node.prev = cur.prev;
- cur.prev = node;
-
- }
-
- private ListNode searchIndex(int index) {
- ListNode cur = head;
- while (index > 0) {
- cur = cur.next;
- index--;
- }
-
- return cur;
- }
-
-
- /**
- * 我写的这个删除的代码将寻找data==Key的这个过程封装成一个方法 思路很好 但是不利于写删除所有的key值的方法
- * 因为想要删除所有的key值,不可避免的是要遍历整个链表,而不是得到某一个节点
- * @param key
- * @return
- */
-
-
- /* //删除第一次出现关键字为key的节点
- public void remove(int key){
- // 不能这样写,因为要求的是删除第一次出现的key
- // if(last.data == key) {
- // last = last.prev;
- // last.next = null;
- // return;
- // }
- // 使cur为data为key的结点
- ListNode cur = findIndexOf(key);
- if (cur == null) {
- System.out.println("没有你要删除的结点");
- return;
- }
- if (cur == head) {
- head = head.next;
- if (head != null) {
- head.prev = null;
- }else {
- // 处理只有一个结点的情况 head直接为空了 此时只需把last也置空即可
- last = null;
- }
- }else {
- // 处理中间结点和尾巴结点 尾巴结点要单独处理(后继为null)
- if (cur.next != null) {
- cur.prev.next = cur.next;
- cur.next.prev = cur.prev;
- }else {
- cur.prev.next = null;
- last = cur.prev;
- }
- }
- }
- private ListNode findIndexOf(int key) {
- ListNode cur = head;
- while (cur != null) {
- if (cur.data == key) {
- return cur;
- }
- cur = cur.next;
- }
- return null;
- }*/
-
- /**
- * 重写remove方法
- * @param key
- * @return
- */
-
- public void remove(int key) {
- ListNode cur = head;
-
- while (cur != null) {
- if (cur.data == key) {
- // 等于key也要分两种情况
- if (cur == head) {
- head = head.next;
- if (head == null) {
- last = null;
- }else {
- head.prev = null;
- }
- }else {
- // 处理中间结点和尾结点
- if (cur.next != null) {
- cur.prev.next = cur.next;
- cur.next.prev = cur.prev;
- }else {
- cur.prev.next = null;
- last = cur.prev;
- }
- }
-
- return;
- }else {
- cur = cur.next;
- }
- }
-
- }
- //删除所有值为key的节点
- public void removeAllKey(int key){
- ListNode cur = head;
-
- while (cur != null) {
- if (cur.data == key) {
- // 等于key也要分两种情况
- if (cur == head) {
- head = head.next;
- if (head == null) {
- last = null;
- }else {
- head.prev = null;
- }
- }else {
- // 处理中间结点和尾结点
- if (cur.next != null) {
- cur.prev.next = cur.next;
- cur.next.prev = cur.prev;
- }else {
- cur.prev.next = null;
- last = cur.prev;
- }
- }
- cur = cur.next;
-
- //return;
- }else {
- cur = cur.next;
- }
- }
-
- }
-
- public void clear(){
- ListNode cur = head;
- while (cur != null) {
- ListNode curNext = cur.next;
-
- cur.prev = null;
- cur.next = null;
-
- cur = cur.next;
- }
-
- // 头尾结点要单独置空
- head = null;
- last = null;
-
-
-
-
-
- // // 把每一个链表都指控
- // ListNode cur = head;
- // while (cur != null) {
- // cur.prev = null;
- // cur = cur.next;
- // if (cur != null) {
- // cur.prev.next = null;
- // }
- // }
- }
-
- }
- // 无参的构造方法
- public LinkedList() {
- }
-
- // 使用其他集合来构建一个链表
- public LinkedList(Collection extends E> c) {
- this();// 调用上面的构造方法
- addAll(c);// 将集合c中的所有元素填入到创建的链表中
- }
boolean add(E e) 尾插 e
- // This method is equivalent to addLast. 这个方法和addLast是等价的
- public boolean add(E e) {
- linkLast(e);
- return true;
- }
-
- // 尾插
- void linkLast(E e) {
- final LinkedList.Node
l = last;// 让l指向最后一个结点 -
- // 创建一个新的结点
- final LinkedList.Node
newNode = new LinkedList.Node<>(l, e, null); - last = newNode;
- if (l == null)
- first = newNode;
- else
- l.next = newNode;
- size++;
- modCount++;// 用于记录集合被修改的次数
- }
void add(int index, E element) 将 e 插入到 index 位置
- public void add(int index, E element) {
- checkPositionIndex(index);// 检查index是否合法
-
- // 尾插
- if (index == size)
- linkLast(element);
- else
- linkBefore(element, node(index));
- }
-
- // Inserts element e before non-null Node succ. 在succ之前添加一个非空的结点e
- void linkBefore(E e, LinkedList.Node
succ) { - // assert succ != null;
-
- // 设置pred为succ的前驱节点
- final LinkedList.Node
pred = succ.prev; - final LinkedList.Node
newNode = new LinkedList.Node<>(pred, e, succ); - succ.prev = newNode;
- if (pred == null)
- first = newNode;
- else
- pred.next = newNode;
- size++;
- modCount++;
- }
boolean addAll(Collection c) 尾插 c 中的元素
- public boolean addAll(Collection extends E> c) {
- return addAll(size, c);
- }
-
-
- public boolean addAll(int index, Collection extends E> c) {
- checkPositionIndex(index);
-
- // 将集合c转变为数组 并求出他的长度
- Object[] a = c.toArray();
- int numNew = a.length;
- if (numNew == 0)
- return false;
-
- LinkedList.Node
pred, succ; -
-
- if (index == size) {
- // 对list进行尾插集合c
- succ = null;
- pred = last;
- } else {
- // 在指定位置的list中进行c集合的插入
- succ = node(index);
- pred = succ.prev;
- }
-
- // 遍历集合 进行插入
- for (Object o : a) {
- @SuppressWarnings("unchecked") E e = (E) o;
- LinkedList.Node
newNode = new LinkedList.Node<>(pred, e, null); - if (pred == null)
- first = newNode;
- else
- pred.next = newNode;
- pred = newNode;
- }
-
- // 将新加入的结点和原来的结点连接起来
- if (succ == null) {
- // 此处处理的是尾插
- last = pred;
- } else {
- // 处理在指定位置的插入
- pred.next = succ;
- succ.prev = pred;
- }
-
- size += numNew;
- modCount++;
- return true;
- }
-
E remove(int index) 删除 index 位置元素
- public E remove(int index) {
- checkElementIndex(index);
- return unlink(node(index));
- }
-
- E unlink(LinkedList.Node
x) { - // assert x != null;
- final E element = x.item;
- final LinkedList.Node
next = x.next; - final LinkedList.Node
prev = x.prev; -
- // 先处理x.prev
- if (prev == null) {
- // 要删除的结点为头结点 直接让x的下一个结点作为头结点
- first = next;
- } else {
- // 不是头结点 连接x的下一个节点
- prev.next = next;
- x.prev = null;
- }
-
- // 处理x.next
- if (next == null) {
- // 要删除的结点是尾结点
- last = prev;
- } else {
- next.prev = prev;
- x.next = null;
- }
-
- x.item = null;
- size--;
- modCount++;
- return element;
- }
以下操作都十分简单,读者可自行查阅使用,这里不做过多的介绍
E get(int index) 获取下标 index 位置元素
E set(int index, E element) 将下标 index 位置元素设置为 element
boolean contains(Object o) 判断 o 是否在线性表中
int indexOf(Object o) 返回第一个 o 所在下标
List subList(int fromIndex, int toIndex) 截取部分 list
LinkedList的遍历的遍历一般有两种遍历方式
- public static void main(String[] args) {
- LinkedList
list = new LinkedList<>(); - list.add(1); // add(elem): 表示尾插
- list.add(2);
- list.add(3);
- list.add(4);
- list.add(5);
- list.add(6);
- list.add(7);
-
- // for each循环遍历
- for (Integer i : list) {
- System.out.print(i + " ");
- }
-
- // 迭代器
- Iterator iterator = list.iterator();
- while(iterator.hasNext()){
- System.out.println(iterator.next());
- }
- }