摘要
本文主要介绍了链表的基本概念与常见操作,并附带了LeetCode上的几道题目,包括203.移除链表元素、707.设计链表、206.反转链表的解题思路与示例代码。
1、链表理论基础
1.1 概念
链表(Linked List)是一种常见的线性数据结构,它由节点(Node)构成,每个节点包含两部分:数据元素和指向下一个节点的引用(或指针)。链表的基本组成部分如下:
- 节点(Node): 链表的基本单元,包含两个字段,一个用于存储数据,另一个用于指向下一个节点。
- 头节点(Head): 链表的第一个节点,通常用来表示链表的起始点。
- 尾节点(Tail): 链表的最后一个节点,其指针通常为空(null)或者指向一个特殊的终结节点。
链表可以分为多种类型,其中常见的包括:
- 单链表(Singly Linked List): 每个节点只有一个指针,指向下一个节点。
- 双链表(Doubly Linked List): 每个节点有两个指针,分别指向前一个节点和后一个节点。
- 循环链表(Circular Linked List): 最后一个节点的指针指向第一个节点,形成一个闭环。
链表相对于数组的优点包括:
- 动态大小: 链表可以根据需要动态增长或缩小,不需要预先分配固定大小的空间。
- 插入和删除效率高: 在链表中插入或删除节点的操作相对容易,只需要调整节点的指针,不需要像数组那样移动大量元素。
- 内存利用率高: 链表可以根据需要分配内存,不会造成内存浪费。
然而,链表也有一些缺点,主要包括:
- 随机访问效率低: 链表中的元素不是按照连续的内存地址存储的,因此随机访问的效率较低。
- 占用额外的空间: 每个节点需要额外的空间来存储指针信息,相对于数组,链表可能占用更多的内存。
链表在计算机科学中被广泛应用,常用于实现其他数据结构,例如栈、队列和图等。它们也在各种算法和编程问题中发挥着重要的作用。理解链表的基本原理和操作是编写高效算法的关键。
1.2 链表的操作
链表是一种常见的数据结构,它支持一系列基本的操作,包括:
- 插入(Insertion): 向链表中添加新节点。
- 删除(Deletion): 从链表中移除节点。
- 查找(Search): 查找链表中特定值的节点。
- 遍历(Traversal): 遍历整个链表,访问每个节点。
下面是这些操作的详细说明:
1. 插入操作:
- 在头部插入(Insert at the Beginning): 在链表的头部插入一个新节点,将其指针指向原来的头节点。
- 在尾部插入(Insert at the End): 在链表的尾部插入一个新节点,将原来的尾节点的指针指向新节点。
- 在指定位置插入(Insert at a Given Position): 在链表的指定位置插入一个新节点,调整相邻节点的指针。
2. 删除操作:
- 删除头节点(Delete at the Beginning): 移除链表的头节点,将头指针指向下一个节点。
- 删除尾节点(Delete at the End): 移除链表的尾节点,将倒数第二个节点的指针置为空。
- 删除指定节点(Delete a Given Node): 移除链表中指定值的节点,调整前后节点的指针。
3. 查找操作:
- 按值查找(Search by Value): 从链表中查找具有特定值的节点,返回节点或节点的位置。
4. 遍历操作:
- 遍历整个链表(Traverse the Entire List): 从链表的头节点开始,依次访问每个节点,通常使用循环进行遍历。
链表的具体实现可以是单链表、双链表、循环链表等,操作的复杂度取决于链表类型和具体实现方式。链表是许多其他数据结构的基础,如栈和队列,因此对链表的操作和理解对于编写高效的算法非常重要。
2、203.移除链表元素
2.1 思路
(删除)使用虚拟头节点, 比较当前节点的next节点的值,满足条件则删除(cur.next = cur.next.next)
2.2 代码
error-1
public ListNode removeElements(ListNode head, int val) {
ListNode dummy = new ListNode(-1, head);
ListNode cur = dummy;
while(cur != null && cur.next != null) {
if(cur.next.val == val) {
cur.next = cur.next.next;
}
cur = cur.next;
}
return dummy.next;
}
测试用例:输入:head = [7,7,7,7], val = 7 ;输出:[7,7];预期结果:[]
原因:
- 当使用虚拟头节点,删除第一个 7 后,当前节点指向第二个 7,所以第二个 7 没有删除;删除第三个 7 后,当前节点指向第四个 7,所以第四个 7 没有删除
解决方式:
- 删除next节点后,当前节点不用指向next节点,可以继续判断当前节点的next节点
AC
// (删除)使用虚拟头节点, 比较当前节点的next节点的值,满足条件则删除(cur.next = cur.next.next)
public ListNode removeElements(ListNode head, int val) {
ListNode dummy = new ListNode(-1, head);
ListNode cur = dummy;
while (cur != null && cur.next != null) {
if (cur.next.val == val) {
cur.next = cur.next.next;
} else {
cur = cur.next;
}
}
return dummy.next;
}
3、707.设计链表
3.1 思路
定义虚拟头节点 head ,定义链表的长度 size,实现 MyLinkedList() 方法,初始化 MyLinkedList 对象;
定义核心方法
getNode(int index),该方法可以通过 index 获取相应的 node,index 的取值范围是 [-1, size -1],则MyLinkedList类中的其他方法实现思路如下:
- int get(int index):可以通过
getNode(index)获取- void addAtHead(int val):等价于
addAtIndex(0, val)- void addAtTail(int val):等价于
addAtIndex(size, val)- void addAtIndex(int index, int val):可以通过
getNode(index-1)获取前节点,然后插入节点- void deleteAtIndex(int index):可以通过
getNode(index-1)获取前节点,然后删除节点
3.2 代码
error-1
错误的代码:逻辑非常混乱
public ListNode getNode(int index) {
index = index + 1;
if (index < 0 || index > size -1) {
return null;
}
ListNode cur = head;
while (index > 0) {
cur = cur.next;
index--;
}
return cur;
}
正确的代码
public ListNode getNode(int index) {
if (index < -1 || index > size - 1) {
return null;
}
ListNode cur = head;
while (index >= 0) {
cur = cur.next;
index--;
}
return cur;
}
index 的取值范围?如果通过 index 正确获取相应的链表节点?
index 代表链表的中元素的下标,因为使用了虚拟链表,但对于使用者是未知的,取值范围应该在 [-1, size - 1]
- -1 代表虚拟节点
- 0 代表头节点
- size - 1 代表尾节点
所以在判断取值范围应该是
index < -1 || index > size - 1遍历链表取值时,
while (index >= 0)
- index = -1,不进入循环,返回
head- index = 0,进入循环,返回
head.next- index 等于其他值同理
error-2
错误的代码
public void addAtTail(int val) {
addAtIndex(size-1, val);
}
正确的代码
public void addAtTail(int val) {
addAtIndex(size, val);
}
void addAtTail(int val)将一个值为val的节点追加到链表中作为链表的最后一个元素。
void addAtIndex(int index, int val)将一个值为val的节点插入到链表中下标为index的节点之前。如果index等于链表的长度,那么该节点会被追加到链表的末尾。
error-3
错误的代码;在 prev.next == null 情况下 会导致空指针异常
public void deleteAtIndex(int index) {
ListNode prev = getNode(index - 1);
if (prev == null) {
return;
}
prev.next = prev.next.next;
size--;
}
正确的代码
public void deleteAtIndex(int index) {
ListNode prev = getNode(index - 1);
if (prev == null || prev.next == null) {
return;
}
prev.next = prev.next.next;
size--;
}
AC
class DesignLinkedList {
private ListNode head;
private int size;
public DesignLinkedList() {
head = new ListNode(-1);
size = 0;
}
public int get(int index) {
ListNode node = getNode(index);
if (node == null) {
return -1;
}
return node.val;
}
public void addAtHead(int val) {
addAtIndex(0, val);
}
public void addAtTail(int val) {
addAtIndex(size, val);
}
public void addAtIndex(int index, int val) {
ListNode prev = getNode(index - 1);
if (prev == null) {
return;
}
prev.next = new ListNode(val, prev.next);
size++;
}
public void deleteAtIndex(int index) {
ListNode prev = getNode(index - 1);
if (prev == null || prev.next == null) {
return;
}
prev.next = prev.next.next;
size--;
}
public ListNode getNode(int index) {
if (index < -1 || index > size - 1) {
return null;
}
ListNode cur = head;
while (index >= 0) {
cur = cur.next;
index--;
}
return cur;
}
private static class ListNode {
int val;
ListNode next;
ListNode() {
}
ListNode(int val) {
this.val = val;
}
ListNode(int val, ListNode next) {
this.val = val;
this.next = next;
}
}
}
4、206.反转链表
4.1 思路
使用变量prev保存前节点,遍历链表,使得当前节点指向prev,最后返回prev就是反转后的链表
4.2 代码
// 使用变量prev保存前节点,遍历链表,使得当前节点指向prev,最后返回prev就是反转后的链表
public ListNode reverseList(ListNode head) {
ListNode prev = null;
ListNode cur = head;
while(cur != null) {
ListNode next = cur.next;
cur.next = prev;
prev = cur;
cur = next;
}
return prev;
}