今日内容
- 链表理论基础
- leetcode. 203 移除链表元素
- leetcode. 707 设计链表
- leetcode. 206 反转链表
今天的内容也是以前做过的。做之前以为可以速通,结果发现还是花了好久,在一些看似不起眼的地方卡了好久。怎么说呢,还是掌握得不太扎实。
链表理论基础
链表是通过指针串联起来的线性结构,链表中每个节点都由两部分组成:数据和指针。
链表的入口节点被称为链表的头节点 head。
链表类型
一个普通的单链表如下图所示:
除了单链表外,还有其他类型的链表:
- 双链表:每个节点的前后都有指针,分别指向它们的前一个和后一个节点。这种链表可以实现前后双向查询。
- 循环链表:整个链表首尾相连的链表。 循环链表可以解决约瑟夫环问题。
PS. 约瑟夫环问题:N个人围成一圈,第一个人从1开始报数,报到M这个数的将被杀掉,下一个人接着从1开始报。如此反复,最后剩下一个。求最后的胜利者。
链表存储方式
与数组不同,链表并不要求用连续的内存空间存储,因为链表节点中的指针将位于不同内存位置上的节点连接起来了。
链表的代码定义
刷题时,平台都会默认给出链表节点的定义。但仍然需要掌握它的定义代码。
下面是Java版本的定义:
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;
}
}
链表的操作
链表的操作也支持增删改查,“改查”操作没有什么特殊的,主要是“增删”操作。
实现链表的“增删”都需要对指针进行操作:
- 增加节点
- 删除节点
在Java中,因为其内存回收机制,所以在删除链表节点后,被删除节点无需我们手动释放内存。
链表性能
如下表所示:
可以发现,链表在做插入和删除操作时效率更高,并且没有固定的内存空间,但查询性能差,因为链表查询元素需要遍历整个链表。因此链表适合于数据量不固定,增删操作频繁,且很少去查询的情况。
Leetcode. 203 移除链表元素
移除链表元素,我们自然而然会想到遍历链表,找到对应元素后移除就好。这样也确实可行,但是其中存在一个问题:假如我们要移除头节点怎么办?
可以想象到,处理头节点的方式和处理其他节点的方式是不一样的,因为处理头节点时需要操作head 指针。这样我们自然而然会想:有没有一种方法能对链表中所有元素都使用同样的逻辑进行处理呢?
这样就引入了虚拟头节点的概念。这个概念在日后处理链表时可以带来很多便利。
虚拟头节点就是在原先的头节点前再连接一个节点,这样原先的头节点就相当于其他的普通节点了,之后就可以使用同一逻辑对整个链表的元素进行处理了。
代码如下:
/**
* 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) {
// 创建虚拟头节点
ListNode dummyHead = new ListNode();
dummyHead.next = head;
ListNode p1 = dummyHead;
while (p1.next != null){
if (p1.next.val == val){
p1.next = p1.next.next;
} else {
p1 = p1.next;
}
}
return dummyHead.next; // 注意此处返回的是虚拟头节点的下一个节点,即真正的头节点
}
}
- 时间复杂度:O(n)
- 空间复杂度:O(1)
Leetcode. 707 设计链表
本题不涉及什么算法,主要是帮助熟悉链表的基本操作和构造。
代码如下:
/**
* 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);
*/
// 定义链表节点
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;
}
}
class MyLinkedList {
int size; // 链表长度
ListNode dummyHead; // 链表的虚拟头节点
// 初始化链表
public MyLinkedList(){
size = 0;
dummyHead = new ListNode();
}
// 获取链表下标为index的值
public int get(int index){
if (index < 0 || index >= size){return -1;} // 注意此处index = size的话,就已经越界了
ListNode p = dummyHead;
for (int i = 0; i <= index; i++){ // 此处也是 i = index,因为要取到这个下标对应节点的值
p = p.next;
}
return p.val;
}
// 添加链表元素
public void addAtIndex(int index, int val){
ListNode p = dummyHead;
ListNode newNode = new ListNode(val);
if (index > size){return;}
for (int i = 0; i < index; i++){ // 此处就不是 i = index了,因为指针指向这个节点的话就执行不了题意中的插入操作了
p = p.next;
}
newNode.next = p.next;
p.next = newNode;
size++;
}
// 可以复用addAtIndex
public void addAtHead(int val){
addAtIndex(0, val);
}
public void addAtTail(int val){
addAtIndex(size, val);
}
public void deleteAtIndex(int index){
if (index < 0 || index >= size){return;}
ListNode p = dummyHead;
for (int i = 0; i < index; i++){ // 和插入同理
p = p.next;
}
p.next = p.next.next;
size--;
}
}
- 时间复杂度:涉及 index 的操作为 O(n),其余操作为O(1)
- 空间复杂度:O(n)
Leetcode. 206 反转链表
本题运用了双指针解决。
解题步骤如下:
- 在一开始定义一个指向null的指针prefix,然后再定义一个指向头节点的指针elem。
- 再定义一个指向elem下一个节点的临时指针temp。使头节点的next指针指向prefix。然后prefix被赋值为头节点指针elem,elem被赋值为temp。
- 重复第2步,直到elem指向null。
代码如下:
/**
* 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) {
if (head == null){return head;}
ListNode prefix = null; // 换向后被指的元素
ListNode elem = head; // 指向要换向的元素
while (elem != null){
ListNode temp = elem.next;
elem.next = prefix;
prefix = elem;
elem = temp;
}
return prefix;
}
}
- 时间复杂度:O(n)
- 空间复杂度:O(1)
这题一开始卡了好久,原因是思维惯性直接把头节点当虚拟头节点处理了......所以一开始就直接少了个元素。做题还是要看清楚,不要公式做题。
总结
重新复习一遍链表,对于虚拟头节点还是印象深刻,但是设计链表时就有点卡住了,反转链表时更是直接惯性使用虚拟头节点处理,这两题上的时间花费有点多了。总体来说,还是有所收获的。