链表理论基础
链表是一种通过指针串联在一起的线性结构,每一个节点由两部分组成,一个是数据域一个是指针域(存放指向下一个节点的指针),最后一个节点的指针域指向null(空指针的意思)。
链表的入口节点称为链表的头结点也就是head。
链表的储存方式:可能是连续发布的,也可能不是连续分布的
单链表
单链表节点是由data和next组成,data储存数据,next储存的下一个节点的指针
双链表
双链表节点是由prev,data和next组成,prev储存的上一个节点的指针,data储存数据,next储存的下一个节点的指针
循环链表
特点就是收尾相连,最后一个节点的next指向开始节点
删除节点
以单链表为例(target是要删除的节点)
将要target的上一个节点的next指向target下一个节点的指针
添加节点
以单链表为例(target是要添加的节点)
将index的上一个节点的next指向target,将target的next指向index节点
性能分析
解题的注意事项
关于链表的题我们需要自己定义链表,如果自己定义的话,使用默认构造函数,无法直接对链表进行赋值
链表定义的代码实现:
public class ListNode {
int val;
ListNode next;
ListNode() {}
ListNode(int val) { this.val = val; }
ListNode(int val, ListNode next) { this.val = val; this.next = next; }
}
LeetCode 203.移除链表元素
题目链接 :203.移除链表元素
解题思路
思路一(暴力解法)
根据链表的性质,将val的上一个节点的指针赋予下一个节点。
但是,头节点如果是目的节点的话,我们直接将头节点赋予个下一个节点
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode() {}
* ListNode(int val) { this.val = val; }
* ListNode(int val, ListNode next) { this.val = val; this.next = next; }
* }
*/
class Solution {
public ListNode removeElements(ListNode head, int val) {
while(head != null && head.val == val){
head = head.next;
}
if(head == null){
return head;
}
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;
}
}
注意:
- cur是指的节点并不代表节点的值,我们如果要将指针被赋予为下下一个节点:cur.next = pre.next;。
- 别忘了head为空时,记得直接输出head。
- 我们将前一个节点赋予个后一个节点,代码的实现head->next = head->next->next(C++),java需要分开写设两个指针(cur,pre)pre.next = cur.next;
- 这种解法是pre作为进行遍历比较的节点,而cur是pre前一个节点,不管cur.val等不等于val,cur的值都是往后移动一位,所以cur = cur.next;,而cur = pre。
思路二(虚拟头节点)
因为按第一种思路需要将头节点和非头结点分开进行删除,如果我加入一个虚拟的头节点,我们可以将头节点的删除合并到非头结点中。
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode() {}
* ListNode(int val) { this.val = val; }
* ListNode(int val, ListNode next) { this.val = val; this.next = next; }
* }
*/
class Solution {
public ListNode removeElements(ListNode head, int val) {
if(head == null){
return head;
}
ListNode Humhead = new ListNode(-1,head);
ListNode pre = Humhead;
ListNode cur = Humhead.next;
while(cur != null){
if(cur.val == val){
pre.next = cur.next;
}else{
pre = cur;
}
cur = cur.next;
}
return Humhead.next;
}
}
注意:
1.输出链表时,我们不能直接输出head,head可能已经改变了(头节点就是删除节点)。
2.上面的说法给以换一个角度去理解,上述代码中,我们从始至终都在对Humhead(虚拟头节点)的链表进行修改,除了虚拟头节点都会进行修改,所以真正的头结点可能已经改变。
LeetCode 707.设计链表
题目链接 :707.设计链表
解题思路
思路一(单链表—虚拟头节点)
- 需要先定义一个链表LinkList
- 初始化引入LinkList(0)虚拟头节点,并且定义一个长度size
- get函数我们需要判断index是否有意义,因为index是从0开始,我们遍历时我们需要从0开始遍历,到index停止,然后输出val。
- addAtIndex函数我们先判断插入index与链表的长度的大小,然后需要说明index<0,算作index=0,需要增加链表的长度,然后从0开始遍历到index-1的位置,在进行赋值
- deleteAtIndex函数我们先判断index与size的大小,然后缩小长度,如果index=0,就直接将头节点后移,需要增加链表的长度,然后从0开始遍历到index-1的位置,在进行赋值
- addAtHead和addAtTail可以通过addAtIndex实现
class LinkList {
int val;
LinkList next;
LinkList(){}
LinkList(int val){this.val = val;}
}
class MyLinkedList {
LinkList head;
int size;
public MyLinkedList() {
//初始化虚拟头节点
head = new LinkList(0);
//链表的长度
size = 0;
}
public int get(int index) {
//下标是否有效
if(index < 0 || index >= size){
return -1;
}
LinkList cur = head;
for(int i = 0; i <= index; i++){
cur = cur.next;
}
return cur.val;
}
public void addAtHead(int val) {
addAtIndex(0,val);
}
public void addAtTail(int val) {
addAtIndex(size,val);
}
public void addAtIndex(int index, int val) {
if(index > size){
return;
}
if(index < 0){
index = 0;
}
size++;
LinkList cur = head;
//遍历至index-1
for(int i = 0; i < index; i++){
cur = cur.next;
}
LinkList pre = new LinkList(val);
pre.next = cur.next;
cur.next = pre;
}
public void deleteAtIndex(int index) {
if(index >= size||index < 0){
return;
}
size--;
if(index == 0){
head = head.next;
//将虚拟头节点变成了head,输出链表是从虚拟头节点的下一个节点开始,所以第一个数就被省略了
return;
}
LinkList cur = head;
//遍历至index-1
for(int i = 0; i < index; i++){
cur = cur.next;
}
cur.next = cur.next.next;
}
}
/**
* Your MyLinkedList object will be instantiated and called as such:
* MyLinkedList obj = new MyLinkedList();
* int param_1 = obj.get(index);
* obj.addAtHead(val);
* obj.addAtTail(val);
* obj.addAtIndex(index,val);
* obj.deleteAtIndex(index);
*/
注意:
1.for(int i = 0; i < index; i++){ pred = pred.next; }
图解如下:
2.在进行添加和删除时,长度也许小改变
3.我们使用了虚拟头节点后,我们对链表头部进行修改时,可以通过虚拟头节点覆盖掉头节点,头节点就会被删除,因为我们输出链表是从虚拟头节点的下一个节点进行输出。
思路二(双链表—虚拟头节点)
与以不同之处在与对前指针的赋值,还有虚拟尾结点的设定
class ListNode{
int val;
ListNode prev;
ListNode next;
ListNode(){}
ListNode(int val){this.val = val;}
}
class MyLinkedList {
ListNode head,tail;
int size;
public MyLinkedList() {
size = 0;
//初始化虚拟头节点
head = new ListNode(0);
//初始化虚拟尾节点
tail = new ListNode(0);
//为了防止addAtIndex中currentNode.next.prev = pred;出现空指针报错
head.next = tail;
tail.prev = head;
}
public int get(int index) {
//index是从0开始算,所以index=size不在链表范围内
if(index < 0 || index >= size){
return -1;
}
ListNode currentNode = head;
//从虚拟头节点开始遍历,i <= index,是遍历至index
for(int i = 0; i <= index; i++){
currentNode = currentNode.next;
}
return currentNode.val;
}
// 卡哥的解法更好
// public int get(int index) {
// //判断index是否有效
// if(index<0 || index>=size){
// return -1;
// }
// ListNode cur = this.head;
// //判断是哪一边遍历时间更短
// if(index >= size / 2){
// //tail开始
// cur = tail;
// for(int i=0; i< size-index; i++){
// cur = cur.prev;
// }
// }else{
// for(int i=0; i<= index; i++){
// cur = cur.next;
// }
// }
// return cur.val;
// }
public void addAtHead(int val) {
addAtIndex(0, val);
}
public void addAtTail(int val) {
addAtIndex(size, val);
}
public void addAtIndex(int index, int val) {
if(size < index){
return;
}
if(index < 0){
index = 0;
}
size++;
ListNode currentNode = head;
//遍历至index前一个节点
for(int i = 0; i < index; i++){
currentNode = currentNode.next;
}
ListNode pred = new ListNode(val);
pred.prev = currentNode;
pred.next = currentNode.next;
//设置尾节点是防止下面代码出现空指针报错
currentNode.next.prev = pred;
currentNode.next = pred;
}
public void deleteAtIndex(int index) {
if(index < 0 || index >= size){
return;
}
size--;
ListNode currentNode = head;
//遍历至index前一个节点
for(int i = 0; i < index; i++){
currentNode = currentNode.next;
}
currentNode.next.next.prev = currentNode.next.next;
currentNode.next = currentNode.next.next;
}
}
/**
* Your MyLinkedList object will be instantiated and called as such:
* MyLinkedList obj = new MyLinkedList();
* int param_1 = obj.get(index);
* obj.addAtHead(val);
* obj.addAtTail(val);
* obj.addAtIndex(index,val);
* obj.deleteAtIndex(index);
*/
注意:
1.虚拟头节点一般使用在对链表进行修改时,主要作用,可以防止head为空时出现空指针报错。
2.在代码中出现的return;,我们可以理解成链表从head.next输出。
LeetCode 206.反转链表
题目链接 :206.反转链表
解题思路
思路一(双指针算法)
解题思路如下图:
需要一个临时指针来存放cur.next,为了后面cur向后移
- 双指针解法
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode() {}
* ListNode(int val) { this.val = val; }
* ListNode(int val, ListNode next) { this.val = val; this.next = next; }
* }
*/
class Solution {
public ListNode reverseList(ListNode head) {
ListNode pre = null;
ListNode cur = head;
while(cur != null){
ListNode temp = cur.next;
cur.next = pre;
pre = cur;
cur = temp;
}
return pre;
}
}
2.迭代双指针解法
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode() {}
* ListNode(int val) { this.val = val; }
* ListNode(int val, ListNode next) { this.val = val; this.next = next; }
* }
*/
class Solution {
public ListNode reverseList(ListNode head) {
return reverse(null,head);
}
public ListNode reverse(ListNode pre, ListNode cur){
if(cur == null){
return pre;
}
ListNode temp = cur.next;
cur.next = pre;
return reverse(cur,temp);
}
}
注意:
1.在输出链表时,需要注意cur是等于null才结束循环,所以pre变成头节点,输出时应该从pre开始输出。
建议: 不建议直接写迭代双指针法,可以先写双指针法,然后按照双指针法写迭代双指针法给容易理解
总结
1.虚拟头节点主要应用于对链表的修改。
2.使用虚拟头节点是为了使我们更好的对全链表进行修改,所以我们使用虚拟头节点时,我们需要将代码中所以头节点都看作为虚拟头节点。
3.在对链表节点的顺序问题上,我们一般会采用双指针方法,因为单链表是单向性的,且节点分布不一定连续,无法通过一个指针去遍历,实现两个不相邻的节点进行连接。