什么是链表,链表是一种通过指针串联在一起的线性结构,每一个节点由两部分组成,一个是数据域一个是指针域(存放指向下一个节点的指针),最后一个节点的指针域指向null(空指针的意思)。
链表的入口节点称为链表的头结点也就是head。
节点有一个指针域和数据域【即上图】
单链表中的指针域只能指向节点的下一个节点。
双链表:每一个节点有两个指针域,一个指向下一个节点,一个指向上一个节点。
双链表 既可以向前查询也可以向后查询。
循环链表,顾名思义,就是链表首尾相连。
循环链表可以用来解决约瑟夫环问题。
数组是在内存中是连续分布的,但是链表在内存中可不是连续分布的。
链表是通过指针域的指针链接在内存中各个节点。
所以链表中的节点在内存中不是连续分布的 ,而是散乱分布在内存中的某地址上,分配机制取决于操作系统的内存管理。
这个链表起始节点为2, 终止节点为7, 各个节点分布在内存的不同地址空间上,通过指针串联在一起。
public class ListNode {
// 结点的值
int val;
// 下一个结点
ListNode next;
// 节点的构造函数(无参)
public ListNode() {
}
// 节点的构造函数(有一个参数)
public ListNode(int val) {
this.val = val;
}
// 节点的构造函数(有两个参数)
public ListNode(int val, ListNode next) {
this.val = val;
this.next = next;
}
}
删除D节点,如图所示:
只要将C节点的next指针 指向E节点就可以了。
那有同学说了,D节点不是依然存留在内存里么?只不过是没有在这个链表里而已。
是这样的,所以在C++里最好是再手动释放这个D节点,释放这块内存。
其他语言例如Java、Python,就有自己的内存回收机制,就不用自己手动释放了。
可以看出链表的增添和删除都是O(1)操作,也不会影响到其他节点。
但是要注意,要是删除第五个节点,需要从头节点查找到第四个节点通过next指针进行删除操作,查找的时间复杂度是O(n)。
数组在定义的时候,长度就是固定的,如果想改动数组的长度,就需要重新定义一个新的数组。
链表的长度可以是不固定的,并且可以动态增删, 适合数据量不固定,频繁增删,较少查询的场景。
203. 移除链表元素 - 力扣(LeetCode)
1.利用虚拟头结点记录头结点的位置
2.利用前驱节点pre,避免再次遍历
在删除时,利用pre.next=node.next即可完美跳过node节点,达到删除node的作用
public class remove_linked_list_elements {
//链表定义:
class ListNode{
int val;
ListNode next;
public ListNode(){
}
//有参构造
public ListNode(int val,ListNode next){
this.val=val;
this.next=next;
}
}
//给你一个链表的头节点 head 和一个整数 val ,请你删除链表中所有满足 Node.val == val 的节点,并返回 新的头节点 。
public ListNode removeElements(ListNode head, int val) {
//如果链表为空
if(head==null){
return head;
}
//首先遍历链表咯?
//首先来个虚拟头结点,防止丢失头结点
ListNode dummy=new ListNode();
dummy.next=head;
//来个前节点
ListNode pre=new ListNode();
pre=dummy;
while (head!=null){
//判断当前节点 是否为val
if(head.val==val){
ListNode next=head.next;
pre.next=next;
head=head.next;
continue;
}
head=head.next;
pre=pre.next;
}
return dummy.next;
}
}
707. 设计链表 - 力扣(LeetCode)
package LinkedList;
//设计链表
public class design_linked_list {
//这里使用单链表
public static class MyLinkedList {
//定义节点 单链表使用ListNode
class ListNode{
int val;
ListNode next;
public ListNode(int val){
this.val=val;
}
}
//这里去记录虚拟头节点,注意这里并不是第一个元素,这个头结点是空点
ListNode dummy;
//记录单链表长度
int size=0;
public MyLinkedList() {
this.size=0;
//todo:1.这里去给虚拟头节点赋值,不然会导致不能使用dummy.next 因为dummy是null时不能使用.next
this.dummy=new ListNode(0);
}
// 获取链表中下标为 index 的节点的值。如果下标无效,则返回 -1 。
public int get(int index) {
//先判断是不是超出链表
//这里因为当链表有一个元素时,链表size=1,而index下标是0,所以index=size时,链表也没有该下标
if(index>=size) {
return -1;
}
//一般这里是先去获取一个值,避免对虚拟头节点直接进行操作,保护虚拟头结点
ListNode cur=dummy; //todo:2、这里去copy dummy 而不是next
//遍历达到index节点
for (int i = 0; i <=index; i++) {
cur=cur.next;
}
size++;
return cur.val;
}
// 将一个值为 val 的节点插入到链表中第一个元素之前。在插入完成后,新节点会成为链表的第一个节点。
public void addAtHead(int val) {
ListNode node=new ListNode(val);
//首先判断链表是否为空
if(size==0){
dummy.next=node;
size++;
return;
}
//非空
//先记录原先的头结点
ListNode head=dummy.next;
dummy.next=node;
node.next=head;
size++;
return;
}
// 将一个值为 val 的节点追加到链表中作为链表的最后一个元素。
public void addAtTail(int val) {
ListNode node=new ListNode(val);
//首先判断链表是否为空
if(size==0){
dummy.next=node;
size++;
return;
}
//去寻找原先的尾节点
ListNode cur=dummy;
while (cur.next!=null){
cur=cur.next;
}
//现在这里的cur就是尾节点
cur.next=node;
size++;
return;
}
//将一个值为 val 的节点插入到链表中下标为 index 的节点之前。
// 如果 index 等于链表的长度,那么该节点会被追加到链表的末尾。
// 如果 index 比长度更大,该节点将 不会插入 到链表中。
public void addAtIndex(int index, int val) {
//如果index=长度
if(index==size){
addAtTail(val);
return;
}
if(index>size){
return;
}
//现在下面是正常插入
ListNode node=new ListNode(val);
ListNode cur=dummy;
//这里i是
for (int i = 0; i < index; i++) {
cur=cur.next;
}
//原先的下一节点
ListNode curNext=cur.next;
cur.next=node;
node.next=curNext;
size++;
return;
}
//如果下标有效,则删除链表中下标为 index 的节点。
public void deleteAtIndex(int index) {
if(index<0||index>=size){
return;
}
ListNode cur=dummy;
//找到下标为index-1的节点 即index是他的下一节点
for (int i = 0; i < index; i++) {
cur=cur.next;
}
ListNode indexNext=cur.next.next;
cur.next=indexNext;
size--;
return;
}
}
public static void main(String[] args) {
MyLinkedList obj = new MyLinkedList();
int param_1 = obj.get(0);
obj.addAtHead(1);
obj.addAtTail(3);
obj.addAtIndex(1,2);
obj.deleteAtIndex(1);
}
}
1.这里去给虚拟头节点赋值,不然会导致不能使用dummy.next 因为dummy是null时不能使用.next
public MyLinkedList() {
this.size=0;
//todo:1.这里去给虚拟头节点赋值,不然会导致不能使用dummy.next 因为dummy是null时不能使用.next
this.dummy=new ListNode(0);
}
2.在进行操作时,这里直接去复制dummy即可
//一般这里是先去获取一个值,避免对虚拟头节点直接进行操作,保护虚拟头结点
ListNode cur=dummy; //todo:2、这里去copy dummy 而不是next
3.注意边界条件(注意模拟) 这里的i从0达到下标为index时,应该循环<=index,可以在纸上模拟
//遍历达到index节点
for (int i = 0; i <=index; i++) {
cur=cur.next;
}
4.关于链表和节点的定义不要搞混
节点的定义:
class ListNode{
int val;
ListNode next;
public ListNode(int val){
this.val=val;
}
}
链表的定义
public static class MyLinkedList {
//定义节点 单链表使用ListNode
class ListNode{
int val;
ListNode next;
public ListNode(int val){
this.val=val;
}
}
//这里去记录虚拟头节点,注意这里并不是第一个元素,这个头结点是空点
ListNode dummy;
//记录单链表长度
int size=0;
//链表的无参构造
public MyLinkedList() {
this.size=0;
//todo:1.这里去给虚拟头节点赋值,不然会导致不能使用dummy.next 因为dummy是null时不能使用.next
this.dummy=new ListNode(0);
}
public static class MyLinkedList_double{
//节点的定义[与单链表比多了一个前驱节点,单链表只有后继next节点]
class ListNode{
int val;
// 这里展示不同 这里展示前驱节点 后继节点
ListNode pre,next;
public ListNode(int val){
this.val=val;
this.pre=null;
this.next=null;
}
}
//虚拟头节点
ListNode dummyHead;
//虚拟尾节点
ListNode dummyTail;
//todo:定义虚拟头尾节点的用处
int size;
//链表的空参构造
public MyLinkedList_double(){
this.size=0;
this.dummyHead=new ListNode(0);
this.dummyTail=new ListNode(0);
this.dummyHead.next=dummyTail;
this.dummyTail.pre=dummyHead;
}
// 获取链表中下标为 index 的节点的值。如果下标无效,则返回 -1 。
public int get(int index) {
if(index>=size){
return -1;
}
ListNode cur=dummyHead;
//这里最终得到的是下标为index的节点
for (int i = 0; i <=index; i++) {
cur=cur.next;
}
return cur.val;
}
// 将一个值为 val 的节点插入到链表中第一个元素之前。在插入完成后,新节点会成为链表的第一个节点。
public void addAtHead(int val) {
ListNode node=new ListNode(val);
//原先的头节点
ListNode head=dummyHead.next;
dummyHead.next=node;
node.pre=dummyHead;
node.next=head;
head.pre=node;
size++;
}
// 将一个值为 val 的节点追加到链表中作为链表的最后一个元素。
public void addAtTail(int val) {
ListNode node=new ListNode(val);
//原来的尾节点
ListNode tail=dummyTail.pre;
tail.next=node;
node.pre=tail;
node.next=dummyTail;
dummyHead.pre=node;
size++;
}
//将一个值为 val 的节点插入到链表中下标为 index 的节点之前。
// 如果 index 等于链表的长度,那么该节点会被追加到链表的末尾。
// 如果 index 比长度更大,该节点将 不会插入 到链表中。
public void addAtIndex(int index, int val) {
ListNode node=new ListNode(val);
if(size==index){
addAtTail(val);
return;
}
if(index>size){
return;
}
ListNode cur=dummyHead;
//todo:这里最终得到的是下标为index的节点
for (int i = 0; i <= index; i++) {
cur=cur.next;
}
//得到原先的index的前驱节点
ListNode oldPre=cur.pre;
oldPre.next=node;
cur.pre=node;
node.pre=oldPre;
node.next=cur;
size++;
}
//如果下标有效,则删除链表中下标为 index 的节点。
public void deleteAtIndex(int index) {
if(index<0||index>=size){
return;
}
ListNode cur=dummyHead;
for (int i = 0; i <= index; i++) {
cur=cur.next;
}
cur.pre.next=cur.next;
cur.next.pre=cur.pre;
size--;
}
}
特点:有虚拟头尾节点、ListNode中有前驱和后继节点
1.双链表的好处就是:在添加首节点和尾节点时,不用考虑size=0的特殊情况,因为已经定义了虚拟头尾节点,不会出现为null的情况
2.虚拟头尾节点(哨兵节点)的作用:
核心思想:用两个不存储实际数据的节点(dummyHead
和 dummyTail
)作为链表的边界,让所有操作都在“内部节点”上进行。
1.虚拟头节点dummy
定义了这个后,方便对head进行操作,防止head移动后,找不到头结点的位置【比如在进行循环时,head=head.next】这样子就会导致原来的头结点找不到了,但是在操作前,让ListNode cur=dummy 将dummy节点复制给cur,由于dummy是虚拟头节点,所以当cur的next指针变了,dummy也变了
2.前驱节点pre
在单链表中定义了这个,在每次移动时记录这个前节点pre,当前节点node,在进行操作时就可以避免再遍历一次
3.在单链表代码中,记得不要直接对head进行操作,而是将head赋值给cur,再对cur进行操作即可
ListNode cur=head;
//后续对cur进行操作,保留head