上节我们已经熟悉了ArrayList的使用,并且进行了简单模拟实现。通过源码知道,ArrayList底层使用数组来存储元素:
public class ArrayList extends AbstractList
implements List, RandomAccess, Cloneable, java.io.Serializable
{
// ...
// 默认容量是10
private static final int DEFAULT_CAPACITY = 10;
//...
// 数组:用来存储元素
transient Object[] elementData; // non-private to simplify nested class access
// 有效元素个数
private int size;
public ArrayList(int initialCapacity) {
if (initialCapacity > 0) {
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
this.elementData = EMPTY_ELEMENTDATA;
} else {
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
}
}
// ...
}
由于其底层是一段连续空间,当在ArrayList任意位置插入或者删除元素时,就需要将后序元素整体往前或者往后搬移,时间复杂度为O(n),效率比较低,因此ArrayList不适合做任意位置插入和删除比较多的场景。因此:java集合中又引入了LinkedList,即链表结构。
链表是一种物理存储结构上非连续存储结构,数据元素的逻辑顺序是通过链表中的引用链接次序实现的。
实际中链表的结构非常多样,以下情况组合起来就有8种链表结构:
单向或者双向
带头或者不带头
循环或者非循环
这里对于带头和不带头要注意区分一下 , 带头链表中链表的头节点是固定不变的且头节点的数值域是虚拟的 (无效的 , 不存放数据) , 不管数据在哪里插入和删除 , 头节点都不会变化 ; 而不带头链表 , 链表的第一个节点 (头节点) 是有效节点 , 数值域是有效的 , 如果在不带头链表中进行头插或者删除第一个节点 , 头节点会发生变化 .
虽然有这么多的链表的结构,但是我们重点掌握两种:
无头单向非循环链表:结构简单,一般不会单独用来存数据。实际中更多是作为其他数据结构的子结构,如哈希桶、图的邻接表等等。另外这种结构在笔试面试中出现很多。
无头双向链表:在Java的集合框架库中LinkedList底层实现就是无头双向循环链表。
下图所示为无头单向非循环链表的结构
/**
* Created with IntelliJ IDEA.
* Description:
* User:YY
* Date:2023-02-11
* Time:11:30
*/
public class MySingleList {
/**
* 结点内部类
*/
static class ListNode{
public int val;
public ListNode next;
public ListNode(int val) {
this.val = val;
}
}
public ListNode head; // 默认 就是NULL
/**
* 默认从头开始打印
*/
public void display(){
ListNode cal = this.head;
while(cal != null){
System.out.print(cal.val + " ");
cal = cal.next;
}
System.out.println();
}
/**
* 用返回值 的display
* 从指定节点newHead打印链表
* @param newHead
*/
public void display(ListNode newHead){
ListNode cal = newHead;
while(cal != null){
System.out.print(cal.val + " ");
cal = cal.next;
}
System.out.println();
}
public int size() {
ListNode cal = this.head;
int count = 0;
while(cal != null){
count++;
cal = cal.next;
}
return count;
}
//头插法
public void addFirst(int data){
ListNode node = new ListNode(data);
node.next = this.head;
this.head = node;
}
//尾插法
public void addLast(int data){
ListNode node = new ListNode(data);
ListNode cal = this.head;
if(cal == null){
this.head = node;
}else {//找出最后一个节点
while(cal.next != null){
cal = cal.next;
}
cal.next = node;
}
}
//任意位置插入,第一个数据节点为0号下标
public void addIndex(int index,int data){
if(index == 0) {
addFirst(data);
return;
}
if(index == this.size()){
addLast(data);
return;
}
if(index < 0 || index > this.size()){
System.out.println("index不合法");
}
//pos合法 且【1,size-1】
ListNode node = new ListNode(data);
//找到需要插入位置的前一个位置
ListNode cal = findCal(index);
//先将插入的指向后面 再讲前面的指向插入的
node.next = cal.next;
cal.next = node;
}
public ListNode findCal(int index){
ListNode cal = this.head;
while(index-1 != 0){
cal = cal.next;
index--;
}
return cal;
}
//查找是否包含关键字key是否在单链表当中
public boolean contains(int key){
ListNode cal = this.head;
while(cal != null){
if(cal.val == key){
return true;
}
cal = cal.next;
}
return false;
}
//删除第一次出现关键字为key的节点
public void remove(int key){
ListNode cal = this.head;
if(cal == null){
System.out.println("空异常");
}
//头位置就查到了 直接结束了
if(cal.val == key){
this.head = cal.next;
return;
}
//从第二个位置开始查
ListNode call = findCall(key);
//没查到
if(call == null) {
System.out.println("没有你要删除的数字!");
return;
}
//将下下一个给下一个
call.next = call.next.next;
}
public ListNode findCall(int key){
ListNode cal = this.head;
//cal从头开始 但从第二个开始比
while (cal.next != null){
if(cal.next.val == key){
return cal;
}
cal = cal.next;
}
return null;
}
/*public void removeAllKey(int key){
ListNode cur = this.head;
while (cur != null){
remove(key);
cur = cur.next;
}
}*/
//删除所有值为key的节点
public void removeAllKey(int key){
if(this.head == null ){
return;
}
//从第二个节点开始判断
ListNode prev = this.head;
ListNode cal = this.head.next;
while (cal != null){
if (cal.val == key){
prev.next = cal.next;
cal = cal.next;
}else {
prev = cal;
cal = cal.next;
}
}
if(this.head.val == key ){
this.head = this.head.next;
}
}
public void clear(){
this.head = null;
}
}
1. 在代码中需要进行遍历链表时 , 要注意区分 cur != null 和 cur.next != null 的使用 , 虽然二者都可以去遍历链表 , 但cur != null , 最后一次循环判断使cur指向为null ; 而cur.next != null 的最后一次循环判断使cur指向的是链表的最后一个节点 .
2. 单链表中插入和删除数据 , 需要先找到要处理位置的上一个位置 , 然后再进行指针指向的修改 .
3. Java当中没有指针的概念 , 这里的节点通过类来实现 , 创建一个引用类型变量 , 这个引用就是Java当中的 “指针” 了 .
4. 单链表中实现清空单链表只需要置空头节点即可 , 要与双链表中的清空区分
下图所示为无头双向非循环链表的结构
import java.util.LinkedList;
/**
* Created with IntelliJ IDEA.
* Description:
* User:YY
* Date:2023-02-14
* Time:14:00
*/
public class MyLinkedList {
static class ListNode {
private int val;
private ListNode prev; //记录前一个节点
private ListNode next; //记录下一个节点
public ListNode(int val) {
this.val = val;
}
}
public ListNode head; // 标记双向链表的头
public ListNode tail; // 标记双向链表的尾巴
//头插法
public void addFirst(int data){
ListNode node = new ListNode(data);
if(this.head == null){
this.head = node;
this.tail = node;
}else {
node.next = this.head;
this.head.prev = node;
this.head = node;
}
}
//尾插法
public void addLast(int data){
ListNode node = new ListNode(data);
if(this.head == null){
this.head = node;
this.tail = node;
}else {
this.tail.next = node;
node.prev = this.tail;
this.tail = node;
}
}
//任意位置插入,第一个数据节点为0号下标
public void addIndex(int index,int data){
//index合法性
if(index < 0 || index > this.size()){
System.out.println("index不合法");
}
//头插
if(index == 0){
addFirst(data);
return;
}
//尾插
if(index == this.size()){
addLast(data);
return;
}
//位置肯定合法 找需要插入的index位置
ListNode cur = findCal(index);
//开始插入
ListNode node = new ListNode(data);
// 前面的next改为node node的prev改为前面的 cur的前面的改为node node后面的改为cur
// 具体怎么造轮子,看自己 保证可以找到前驱后继即可
node.next = cur;
cur.prev.next = node;
node.prev = cur.prev;
cur.prev = node;
/**
* cur.prev.next = node;
* node.prev = cur.prev;
* cur.prev = node;
* node.next = cur;
*/
}
public ListNode findCal(int index){
ListNode cal = this.head;
while(index != 0){
cal = cal.next;
index--;
}
return cal;
}
//查找是否包含关键字key是否在单链表当中
public boolean contains(int key){
ListNode cur = this.head;
while(cur != null){
if(cur.val == key){
return true;
}
cur = cur.next;
}
return false;
}
//删除第一次出现关键字为key的节点
public void remove(int key){
ListNode cur = this.head;
while (cur != null){
//查到的话
if(cur.val == key){
//第一个就查到了
if(cur == this.head){
this.head = this.head.next;
//是不是只有一个节点
if(this.head != null){
this.head.prev = null;
} else {
this.tail = null;
}
}else {//第一个没查到
cur.prev.next = cur.next;
//删除的是不是最后一个节点
if(cur.next != null){
cur.next.prev = cur.prev;
}else {
this.tail = cur.prev;
}
}
return;
}
cur = cur.next;
}
}
//删除所有值为key的节点
public void removeAllKey(int key){
ListNode cur = this.head;
while (cur != null){
//查到的话
if(cur.val == key){
//第一个就查到了
if(cur == this.head){
this.head = this.head.next;
//是不是只有一个节点
if(this.head != null){
this.head.prev = null;
} else {
this.tail = null;
}
}else {//第一个没查到
cur.prev.next = cur.next;
//删除的是不是最后一个节点
if(cur.next != null){
cur.next.prev = cur.prev;
}else {
this.tail = cur.prev;
}
}
}
cur = cur.next;
}
}
//得到单链表的长度
public int size(){
ListNode cur = this.head;
int count = 0;
while(cur != null){
cur = cur.next;
count++;
}
return count;
}
//打印双向链表
public void display(){
ListNode cur = this.head;
while(cur != null){
System.out.print(cur.val + " ");
cur = cur.next;
}
System.out.println();
}
public void display(ListNode newHead){
ListNode cur = newHead;
while(cur != null){
System.out.print(cur.val + " ");
cur = cur.next;
}
System.out.println();
}
public void clear(){
ListNode cur = this.head;
while(cur != null){
ListNode curNext = cur.next;
cur.next = null;
cur.prev = null;
cur = curNext;
}
this.head = null;
this.tail = null;
}
}
1. 与单链表中的插入和删除实现进行区分 , 这里双链表中的插入和删除 , 因为此时链表是双向的 , 所以不需要像单链表一样找要处理位置的前一个位置 , 只需要找到要处理的位置去改变前驱和后记指针指向即可 .
2. 在进行删除元素操作时 , 需要考虑的细节比较多 , 特别需要注意删除头节点与尾节点时的操作(考虑prev为null和next为null , 与删除中间节点不同) , 具体实现看上面给出的代码 .
3. 注意双链表的清空链表实现 , 与单链表中的进行区分 , 双链表中需要手动去将每个节点的两个指针域置为null , 最后再将head和tail去置空 .
203. 移除链表元素 - 力扣(LeetCode)
206. 反转链表 - 力扣(LeetCode)
876. 链表的中间结点 - 力扣(LeetCode)
链表中倒数第k个结点_牛客题霸_牛客网 (nowcoder.com)
21. 合并两个有序链表 - 力扣(LeetCode)
链表的回文结构_牛客题霸_牛客网 (nowcoder.com)
160. 相交链表 - 力扣(LeetCode)
链表分割_牛客题霸_牛客网 (nowcoder.com)
141. 环形链表 - 力扣(LeetCode)
142. 环形链表 II - 力扣(LeetCode)
链表知识点题库 - 力扣(LeetCode)
牛客网在线编程_编程学习|练习题_数据结构|系统设计题库 (nowcoder.com)