什么是链表
- 定义: 数据+指针。最后一个指针指向null。
- 类型:单链表(一个指针);双链表(双指针,指向前后node);循环链表(一个环);
- 储存方式:散乱分布 & 不连续,全靠指针链接,所以分配机制取决于操作系统的内存管理
Coding中如何定义链表?
class ListNode { //类名 :Java类就是一种自定义的数据结构
int val; //数据 :节点数据
ListNode next; //对象 :引用下一个节点对象。在Java中没有指针的概念,Java中的引用和C语言的指针类似
ListNode(int val){ //构造方法 :构造方法和类名相同
this.val=val; //把接收的参数赋值给当前类的val变量
}
}
class Test{
public static void main(String[] args){
ListNode nodeSta = new ListNode(0); //创建首节点
ListNode nextNode; //声明一个变量用来在移动过程中指向当前节点
nextNode=nodeSta; //指向首节点
//创建链表
for(int i=1;i<10;i++){
ListNode node = new ListNode(i); //生成新的节点
nextNode.next=node; //把心节点连起来
nextNode=nextNode.next; //当前节点往后移动
} //当for循环完成之后 nextNode指向最后一个节点,
nextNode=nodeSta; //重新赋值让它指向首节点
print(nextNode); //打印输出
//替换节点
while(nextNode!=null){
if(nextNode.val==4){
ListNode newnode = new ListNode(99); //生成新的节点
ListNode node=nextNode.next.next; //先保存要替换节点的下一个节点
nextNode.next.next=null; //被替换节点 指向为空 ,等待java垃圾回收
nextNode.next=newnode; //插入新节点
newnode.next=node; //新节点的下一个节点指向 之前保存的节点
}
nextNode=nextNode.next;
}//循环完成之后 nextNode指向最后一个节点
nextNode=nodeSta; //重新赋值让它指向首节点
print(nextNode); //打印输出
}
//打印输出方法
static void print(ListNode listNoed){
//创建链表节点
while(listNoed!=null){
System.out.println("节点:"+listNoed.val);
listNoed=listNoed.next;
}
System.out.println();
}
}
- 链表数据结构中常用操作:开头/结尾的增加,删除, 获取,迭代元素
详细教程直通车:https://www.runoob.com/java/java-linkedlist.html
链表和数组的区别?什么时候用?(面试常问)
数组在定义的时候,长度就是固定的,如果想改动数组的长度,就需要重新定义一个新的数组。
链表的长度可以是不固定的,并且可以动态增删, 适合数据量不固定,频繁增删,较少查询的场景
LinkedList
和 ListNode
在数据结构中扮演不同的角色,它们的区别主要体现在以下几个方面:
-
定义和用途:
LinkedList
:LinkedList
通常指的是一个完整的链表数据结构。它表示一系列元素的集合,这些元素以特定的顺序连接在一起。- 在许多编程语言中,比如 Java,
LinkedList
是一个内置的库类,提供了各种链表操作的方法,如添加、删除元素,以及遍历等。
ListNode
:ListNode
通常表示链表中的单个节点。每个节点包含两部分:存储的数据(或值),以及指向链表中下一个节点的指针(在双向链表中,还可能包含指向前一个节点的指针)。ListNode
通常是一个简单的类或结构体,在实现自定义链表结构时使用。LinkedList
是一个更高级别的抽象,它封装了链表的内部细节,提供了一个清晰的接口供外部使用。ListNode
是一个较低级别的构造,它更接近链表的内部实现细节。
常用:
MyLinkList(); 类中 get(index); addAtHead(val);addAtTail(val);addAtIndex(index,val);deleteAtIndex(index)
203-移除链表元素
伪代码:
1)确定单链表还是双链表
2)特殊情况:链表为空的情况;如果删除val 是head的值时
3)创建一个虚拟节点 dummy 使其指向头节点 (哨兵)
4)定义一个指针 pre ,初始化指向虚拟节点 dummy
5)定义一个指针 cur
,初始化为指向链表的头节点。这个指针将用于遍历整个链表
6)开始一个循环,遍历链表直到 cur
指针指向 null
(即链表的末尾)
7)如果当前节点 cur
的值等于要删除的值 val
,则将前一个节点 pre
的 next
指向 cur
的下一个节点,从而跳过 cur
节点,实现删除操作。
8)如果当前节点 cur
的值不等于 val
,则简单地将 pre
移动到 cur
的位置
9)移动 cur
指针到下一个节点,继续遍历
10)循环结束后,返回虚拟节点的下一个节点,即更新后的链表的头节点。这是因为原始的头节点可能在删除过程中被移除
/**
* 添加虚节点方式
* 时间复杂度 O(n)
* 空间复杂度 O(1)
* @param head
* @param val
* @return
*/
public ListNode removeElements(ListNode head, int val) {
if (head == null) {
return head;
}
// 因为删除可能涉及到头节点,所以设置dummy节点,统一操作
ListNode dummy = new ListNode(-1, head);
ListNode pre = dummy;
ListNode cur = head;
while (cur != null) {
if (cur.val == val) {
pre.next = cur.next;
} else {
pre = cur;
}
cur = cur.next;
}
return dummy.next;
}
/**
* 不添加虚拟节点方式
* 时间复杂度 O(n)
* 空间复杂度 O(1)
* @param head
* @param val
* @return
*/
public ListNode removeElements(ListNode head, int val) {
while (head != null && head.val == val) {
head = head.next;
}
// 已经为null,提前退出
if (head == null) {
return head;
}
// 已确定当前head.val != val
ListNode pre = head;
ListNode cur = head.next;
while (cur != null) {
if (cur.val == val) {
pre.next = cur.next;
} else {
pre = cur;
}
cur = cur.next;
}
return head;
}
/**
* 不添加虚拟节点and pre Node方式
* 时间复杂度 O(n)
* 空间复杂度 O(1)
* @param head
* @param val
* @return
*/
public ListNode removeElements(ListNode head, int val) {
while(head!=null && head.val==val){
head = head.next;
}
ListNode curr = head;
while(curr!=null){
while(curr.next!=null && curr.next.val == val){
curr.next = curr.next.next;
}
curr = curr.next;
}
return head;
}
-
ListNode dummy = new ListNode(-1, head);
- 该行创建一个
ListNode
名为 的新对象dummy
。 - 这里的构造函数
ListNode
有两个参数。第一个参数 (-1
) 是分配给该节点的值,第二个参数 (head
) 是对链表中下一个节点的引用。在此上下文中,head
指的是您正在使用的现有链表的第一个节点。 - 该
dummy
节点是“虚拟”或“哨兵”节点。它的主要目的是提供一种简单的方法来处理边缘情况,特别是在列表的开头(例如插入新头或删除当前头),而无需编写额外的条件代码。
- 该行创建一个
-
ListNode pre = dummy;
- 此行创建一个
ListNode
名为 的新引用pre
,并将其指定为引用与 相同的节点dummy
。 pre
现在和都dummy
引用内存中的同一个节点——在第一行中创建的节点。pre
在遍历或修改链表的算法中,节点通常用作指针或迭代器。它可以沿着列表移动,而dummy
停留在开头。这在需要在遍历或修改操作中跟踪当前节点之前的节点的情况下非常有用
- 此行创建一个
707-设计链表
这道题目设计链表的五个接口:
- 获取链表第index个节点的数值
- 在链表的最前面插入一个节点
- 在链表的最后面插入一个节点
- 在链表第index个节点前面插入一个节点
- 删除链表的第index个节点
两种方式:
- 直接使用原来的链表来进行操作。
- 设置一个虚拟头结点在进行操作。
单链表设计:
class ListNode {
int val; //1. 这是一个整数类型的成员变量,用于存储节点的值
ListNode next;//2. 指向下一个节点 (1和2构成一个链表里的一个节点)
ListNode() {
}
ListNode(int val) {
this.val = val;
} //this.val指的是正在创建的对象val的成员变量。
// ListNode等号val右边的 指的是传递给构造函数的参数。
// 因此,这一行将作为参数传递的值分配给val新对象的成员变量
}
//这个类提供了链表的功能实现,如添加、删除和获取节点。
class MyLinkList{
int size;
ListNode head;
public MyLinkedList() {
size = 0;
head = new ListNode(0);
}
public int get(int index) {
if (index = 0 || index >= size) {
reutrn - 1;
}
ListNode currentNode = head; //在 get 方法中,currentNode 用于遍历链表,
// 初始化一个从链表头部开始遍历的指针,以便找到并返回特定索引位置的节点值
/*开始时,它被设置为链表的虚拟头节点(即 head)。
虚拟头节点是一个不存储实际数据的辅助节点,
它的 next 指针指向链表的第一个实际存储数据的节点*/
for (int i = 0; i <= index; i++) {
currentNode = currentNode.next;
}
return currentNode.val;
}
public void addAtHead(int val) {
addAtIndex(0,val);
}
public void addAtTail(int val) {
addAtTail(size,val);
}
// 在第 index 个节点之前插入一个新节点,例如index为0,那么新插入的节点为链表的新头节点。
// 如果 index 等于链表的长度,则说明是新插入的节点为链表的尾结点
// 如果 index 大于链表的长度,则返回空
public void addAtIndex(int index, int val) {
if(index > size){return;}
//在一些数据结构实现中,负索引可能被视为无效,
// 但在这个方法中,任何负索引都被矫正为 0,
// 意味着如果传入负数作为 index,方法会将新节点插入到链表的头部。
if(index<0){
index=0;
}
size++;//在插入新节点后,size(表示链表中节点的数量)增加 1
//请注意,这行代码在两个 if 语句之后,但在实际插入节点的代码之前。
// 这意味着即使在进行索引的有效性检查和调整后,只要方法继续执行,size 就会增加
//第一个 if 语句防止在不合法的位置插入节点,
// 而第二个 if 语句提供了一种容错机制,允许负索引的使用,并将其视为在链表头部插入节点。
// 这两个条件语句共同确保 addAtIndex 方法的正确性和健壮性
ListNode pred = head;
for (int = 0; i <index; i++){
pred = pred.next;
}
ListNode toAdd = new ListNode(val);
tooAdd.next = pred.next;
pred.next = toAdd;
}
public void deleteAtIndex(int index) {
if(index<0||index >=size){return}
}
size--;
ListNode pred =head; //pred 指针指向head
for (int i =0; i < index; i++){
pred =pred.next;
}
pred.next=pred.next.next;
}
双链表设计:
//双链表
class MyLinkLIst{
Class ListNode{
int val;
ListNode next,prev;
ListNode(int x){val=x;} //参数设置
}
int size;
ListNode head,tail;
public MyLinkLIst(){
size = 0;
head = new ListNode(0);
tail = new ListNode(0);
head.next = tail;
tail.prev =head;
}
public int get(int index){
if(index<0)|| index >=size){return -1;}
ListNode cur =head;
// 通过判断 index < (size - 1) / 2 来决定是从头结点还是尾节点遍历,提高效率
if(index<(size-1)/2){
for(int i=0;i<=index; i++){
cur = cur.next;
}
} else{
cur = tail;
for(int i=0;i<=size-index-1;i++){
cur =cur.prev;
}
} return cur.val;
}
public void addAtHead (int val){
ListNode cur = head;
LIstNode newNode= new ListNode(val);
newNode.next=cur.next;
cur.next.prev = newNode; //这行代码的意思是,在双向链表中,设置 cur.next 的前一个节点 (prev) 为 newNode。
//是最关键的部分。这行代码将 cur 的下一个节点的 prev 指针(即 cur.next.prev)设置为 newNode。
// 这样做是为了在链表中正确插入 newNode。
// 这确保了从 newNode 向前看能够回到 cur 节点,保持了双向链表的完整性
cur.next = newNode; //令cur的指针指向新添加的newNode
newNode.prev = cur;
size++;
}
public void addAtTail(int val){
ListNode cur = tail;
ListNode newNode = new ListNode(val);
newNode.prev=tail;
newNode.prev = cur.prev;
cur.prev.next = newNode;
cur.prev = newNode;
size++;
}
public void addAtIndex(int val){
if (index>size){return;}
if (index<0){index = 0;}
ListNode cur = head;
for (int i=0;i<index ;i++){
cur = cur.next;
}
ListNode newNode = new ListNode(val);
newNode.next = cur.next;
cur.next.prev = newNode; //分别设置newNode的前后指针val
newNode.prev = cur;
cur.next = newNode;//分别设置原来位置cur的前后指针val
size++; //插入一个数值就
}
public void deleteAtIndex(int val){
if(index>=size||index<0){return;}
ListNode cur = head;
for (int i=0; i <index ; i++){
cur = cur.next;
}
cur.next.next.prev = cur;
cur.next= cur.next.next;
size--;
}
}
206-反转链表
两种方法:双指针和递归(内层逻辑也是双指针方法)
- 初始化
- pre 设置为null,因为反转之后 pre是尾节点指向的null;
- tmp 是 动态的临时保存cur指向next (方便反转过来的时候用)
- cur 是头节点 (这样cur 前后都有保障)
- 遍历 --->思考什么是终止条件 (cur是null的时候)
- 开始循环
- (注意这里代码的顺序)先保存cur的下一个节点 (用tmp)
- 开始反转 cur的指向pre
- 再将 pre 赋值 cur
- 再将cur 赋值 tmp
//双指针法:
public ListNode reverseList(ListNode head) {
ListNode cur = head;
ListNode prev = null;
ListNode tmp = null;
while (cur!=null){
tmp = cur.next;
cur.next = prev;
prev = cur;
cur = tmp;
}
return prev;
}
//递归方法 (引用自K神的答案,真的很简洁明了
class Solution {
public ListNode reverseList(ListNode head) {
return recur(head, null); // 调用递归并返回
}
private ListNode recur(ListNode cur, ListNode pre) {
if (cur == null) return pre; // 终止条件
ListNode res = recur(cur.next, cur); // 递归后继节点
cur.next = pre; // 修改节点引用指向
return res; // 返回反转链表的头节点
}
}
public ListNode reverseList(ListNode head)
- 这是一个公开方法,用于初始化反转链表的过程。它接受一个ListNode
类型的参数head
,这是要反转的链表的头节点。
return reverse(null, head);
- 在reverseList
方法中调用了私有的递归方法reverse
,传递两个参数:null
和头节点head
。这里,null
是作为前一个节点(prev
)传递的,因为初始时没有前一个节点
private ListNode reverse(ListNode prev, ListNode cur)
- 这是一个私有的递归方法,用于实际执行链表的反转。它接受两个参数:prev
(前一个节点)和cur
(当前节点)
if (cur == null)
- 这是递归的基本情况。当cur
为null
时,意味着链表已经遍历完成,因此返回prev
,这时prev
是反转后链表的头节点。
recur
方法在每次递归调用后立即返回反转链表的头节点(res
)。这意味着res
在整个递归过程中一直保持不变,始终是最终反转链表的头节点
递归调用recur(cur.next, cur)
先于节点反转操作cur.next = pre
。这是一种“自顶向下”的递归方式,先递归到链表的末尾,然后在回溯过程中进行节点反转。