链表是一种动态数据结构,其中每个元素(称为节点)包含两部分:数据和指向下一个节点的指针。链表中的第一个节点称为头节点,最后一个节点称为尾节点。链表中的节点可以在任何位置插入或删除,而无需移动其他节点。
在链表中,每个节点都存储一个数据元素,以及一个指向下一个节点的指针。这个指针可以指向一个节点,也可以指向一个空节点(表示链表的结尾)。链表的头节点和尾节点分别表示链表的开始和结束。
链表的优点在于它允许在运行时插入和删除节点,从而可以动态地调整其长度。与数组相比,链表的内存使用是动态的,不需要预先分配固定大小的空间。但是,链表的访问速度较慢,因为需要遍历链表以查找特定元素。
给你一个链表的头节点 head
和一个整数 val
,请你删除链表中所有满足 Node.val == val
的节点,并返回 新的头节点 。
示例 1:
输入:head = [1,2,6,3,4,5,6], val = 6
输出:[1,2,3,4,5]
示例 2:
输入:head = [], val = 1
输出:[]
示例 3:
输入:head = [7,7,7,7], val = 7
输出:[]
提示:
[0, 104]
内1 <= Node.val <= 50
0 <= val <= 50
思路解析1:当我们使用原始的链表进行删除时,要考虑两种情况,第一种是删除的为普通节点的话,之间让该节点指向下一个节点的下一个节点(p.next = p.next.next)。第二种情况是当我们删除的节点是头节点的时候,我们要将头节点后移一位即可。注意当我们删除节点时,要保证该节点的前后都有节点并且要知道上一个节点是什么,即删除元素必有前驱。
/**
* 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 p = head;
while(p.next != null){
if(p.next.val == val){
p.next = p.next.next;
}
else{
p = p.next;
}
}
return head;
}
}
**思路解析:**使用虚拟头节点的情况,我们在该链表的前面,加入一个虚拟头节点,这样原来链表的头节点就变成了普通节点,就没有了特殊的情况,这样我们去进行遍历,注意返回头节点的时候是虚拟节点的下一个元素,即dummy.next。
/**
* 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 dummy = new ListNode(-1,head);
ListNode c = dummy;
while(c.next != null){
if(c.next.val == val){
c.next = c.next.next;
}
else{
c = c.next;
}
}
return dummy.next;
}
}
你可以选择使用单链表或者双链表,设计并实现自己的链表。
单链表中的节点应该具备两个属性:val
和 next
。val
是当前节点的值,next
是指向下一个节点的指针/引用。
如果是双向链表,则还需要属性 prev
以指示链表中的上一个节点。假设链表中的所有节点下标从 0 开始。
实现 MyLinkedList
类:
MyLinkedList()
初始化 MyLinkedList
对象。int get(int index)
获取链表中下标为 index
的节点的值。如果下标无效,则返回 -1
。void addAtHead(int val)
将一个值为 val
的节点插入到链表中第一个元素之前。在插入完成后,新节点会成为链表的第一个节点。void addAtTail(int val)
将一个值为 val
的节点追加到链表中作为链表的最后一个元素。void addAtIndex(int index, int val)
将一个值为 val
的节点插入到链表中下标为 index
的节点之前。如果 index
等于链表的长度,那么该节点会被追加到链表的末尾。如果 index
比长度更大,该节点将 不会插入 到链表中。void deleteAtIndex(int index)
如果下标有效,则删除链表中下标为 index
的节点。示例:
输入
["MyLinkedList", "addAtHead", "addAtTail", "addAtIndex", "get", "deleteAtIndex", "get"]
[[], [1], [3], [1, 2], [1], [1], [1]]
输出
[null, null, null, null, 2, null, 3]
解释
MyLinkedList myLinkedList = new MyLinkedList();
myLinkedList.addAtHead(1);
myLinkedList.addAtTail(3);
myLinkedList.addAtIndex(1, 2); // 链表变为 1->2->3
myLinkedList.get(1); // 返回 2
myLinkedList.deleteAtIndex(1); // 现在,链表变为 1->3
myLinkedList.get(1); // 返回 3
提示:
0 <= index, val <= 1000
get
、addAtHead
、addAtTail
、addAtIndex
和 deleteAtIndex
的次数不超过 2000
。单链表实现
// 单链表
class ListNode {
int val;
ListNode next;
ListNode(){}
ListNode(int val) {
this.val=val;
}
}
// 自定义的链表类,包含size属性,用于存储链表元素的个数,以及虚拟头结点head,用于方便操作链表
class MyLinkedList {
// size存储链表元素的个数
int size;
// 虚拟头结点
ListNode head;
// 初始化链表
public MyLinkedList() {
size = 0;
head = new ListNode(0);
}
// 获取第index个节点的数值,注意index是从0开始的,第0个节点就是头结点
public int get(int index) {
// 如果index非法,返回-1
if (index < 0 || index >= size) {
return -1;
}
ListNode currentNode = head;
// 包含一个虚拟头节点,所以查找第 index+1 个节点
for (int i = 0; i <= index; i++) {
currentNode = currentNode.next;
}
return currentNode.val;
}
// 在链表最前面插入一个节点,等价于在第0个元素前添加
public void addAtHead(int val) {
addAtIndex(0, val);
}
// 在链表的最后插入一个节点,等价于在(末尾+1)个元素前添加
public void addAtTail(int val) {
addAtIndex(size, val);
}
// 在第 index 个节点之前插入一个新节点,例如index为0,那么新插入的节点为链表的新头节点。
// 如果 index 等于链表的长度,则说明是新插入的节点为链表的尾结点
// 如果 index 大于链表的长度,则返回空
public void addAtIndex(int index, int val) {
if (index > size) {
return;
}
if (index < 0) {
index = 0;
}
size++;
// 找到要插入节点的前驱
ListNode pred = head;
for (int i = 0; i < index; i++) {
pred = pred.next;
}
ListNode toAdd = new ListNode(val);
toAdd.next = pred.next;
pred.next = toAdd;
}
// 删除第index个节点
public void deleteAtIndex(int index) {
if (index < 0 || index >= size) {
return;
}
size--;
if (index == 0) {
head = head.next;
return;
}
ListNode pred = head;
for (int i = 0; i < index ; i++) {
pred = pred.next;
}
pred.next = pred.next.next;
}
}
双链表
//双链表
class ListNode{
int val;
ListNode next,prev;
ListNode() {};
ListNode(int val){
this.val = val;
}
}
class MyLinkedList {
//记录链表中元素的数量
int size;
//记录链表的虚拟头结点和尾结点
ListNode head,tail;
public MyLinkedList() {
//初始化操作
this.size = 0;
this.head = new ListNode(0);
this.tail = new ListNode(0);
//这一步非常关键,否则在加入头结点的操作中会出现null.next的错误!!!
head.next=tail;
tail.prev=head;
}
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) {
//等价于在第0个元素前添加
addAtIndex(0,val);
}
public void addAtTail(int val) {
//等价于在最后一个元素(null)前添加
addAtIndex(size,val);
}
public void addAtIndex(int index, int val) {
//index大于链表长度
if(index>size){
return;
}
//index小于0
if(index<0){
index = 0;
}
size++;
//找到前驱
ListNode pre = this.head;
for(int i=0; i<index; i++){
pre = pre.next;
}
//新建结点
ListNode newNode = new ListNode(val);
newNode.next = pre.next;
pre.next.prev = newNode;
newNode.prev = pre;
pre.next = newNode;
}
public void deleteAtIndex(int index) {
//判断索引是否有效
if(index<0 || index>=size){
return;
}
//删除操作
size--;
ListNode pre = this.head;
for(int i=0; i<index; i++){
pre = pre.next;
}
pre.next.next.prev = pre;
pre.next = pre.next.next;
}
}
给你单链表的头节点 head
,请你反转链表,并返回反转后的链表。
示例 1:
输入:head = [1,2,3,4,5]
输出:[5,4,3,2,1]
示例 2:
输入:head = [1,2]
输出:[2,1]
示例 3:
输入:head = []
输出:[]
提示:
[0, 5000]
-5000 <= Node.val <= 5000
**进阶:**链表可以选用迭代或递归方式完成反转。你能否用两种方法解决这道题?
**思路解析:**对于链表来说,由于链表都是由指针指向的,所以我们只要将指针反转一下就可以达到目的,我们可以采用双指针的方法,一个指针用来遍历,另一个指针用来反转方向。在题目中我们还需要借助一个temp指针进行存储指向位置。
/**
* 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 p = null;
ListNode c = head;
ListNode temp = null;
while(c != null){
temp = c.next; //用来保存指向下一个节点
c.next = p; //反转这个节点的指针指向
p = c; //向前遍历 p++ c++
c = temp;
}
return p;
}
}
**思路解析:**我们可以采用递归的算法,递归的算法和双指针的算法思路一致。按照递归思路进行解析
reverse(c , p){
if(c==null){ //当c 为空时,返回p
return p;
}
temp = c.next; //这里我们依然需要借助temp存储值。
c.next = p;
return reverse(c,temp); //由双指针算法可知,进行 遍历p++ c++为 p = c; c = temp; //所以对应reverse为 reverse(c,temp)
}
递归实现
class Solution {
public ListNode reverseList(ListNode head) {
return reverse(null, head);
}
private ListNode reverse(ListNode p, ListNode c) {
if (c == null) {
return p;
}
ListNode temp = null;
temp = c.next;// 先保存下一个节点
c.next = p;// 反转
return reverse(c, temp);
}
}
p); //由双指针算法可知,进行 遍历p++ c++为 p = c; c = temp; //所以对应reverse为 reverse(c,temp)
}
递归实现
```java
class Solution {
public ListNode reverseList(ListNode head) {
return reverse(null, head);
}
private ListNode reverse(ListNode p, ListNode c) {
if (c == null) {
return p;
}
ListNode temp = null;
temp = c.next;// 先保存下一个节点
c.next = p;// 反转
return reverse(c, temp);
}
}