链表基础
链表就是通过指针来串联的一个线性数据结构,每一个元素都是节点对象。节点由数据和存放下一个节点的指针组成。入口节点为head,最后一个节点指向null。
链表也有不同的类型,如单链表(只有指向下一个节点的指针),双链表(有指向前/后两个节点的指针),循环链表(链表最后一个节点指向head)
链表和数组不同,链表在计算机中存储的位置不是连续而是随机的,它们只是通过不同节点对象指针指向的地址来访问。
LeetCode - 203. Remove Linked List Elements 移除链表元素
移除链表中的元素,本质上就是改动移除元素的前一个节点的下个指针直接指向移除元素的下一个节点。这道题用dummy节点会比较方便,但是head也有可能是删除对象,由于我们判断移除元素是通过检查next指针是否是删除对象,因此在head之前的虚拟头节点开始循环会使删除逻辑比较统一,否则我们需要单独写一个检查头节点的逻辑。
在我第一次写的时候疏忽了一个细节,在判断下一个节点是删除对象并且改动next指向的时候不应该把current向后移,因为有可能next所指向的新的节点也是删除对象,直接后移就相当于跳过了,因此要保证检查完下一个不是删除对象的时候current才能往后移。
class Solution {
public ListNode removeElements(ListNode head, int val) {
ListNode dummy = new ListNode(-1);
dummy.next = head;
ListNode current = dummy;
while (current.next != null) {
if (current.next.val == val) {
current.next = current.next.next;
} else {
current = current.next;
}
}
return dummy.next;
}
}
LeetCode - 707. Design Linked List 设计链表
这道题是理解链表增删改查操作最好的案例。注意插入节点的操作常犯的错就是步骤的问题,可能会直接把前一个节点之间连接到新的节点,但是新的节点想要链接到下一个节点的时候发现取不到了。因此应该要先保存当前节点的下一个节点再进行连接新节点的操作。
链表的操作同样要注意边界处理的问题,addAtIndex方法中的index其实可以等于size,也就是等于在末尾添加元素。
//自己定义的ListNode类
class ListNode {
int val;
ListNode next;
ListNode(){};
ListNode(int val) {
this.val = val;
}
}
class MyLinkedList {
int size; //存放元素个数 / 链表长度
ListNode dummy; //虚拟头节点
public MyLinkedList() {
size = 0;
dummy = new ListNode(-1); //实例化的时候自动先生产一个虚拟头节点
}
public int get(int index) {
// index from 0 (head node)
if (index < 0 || index > size - 1) return -1;
ListNode current = dummy;
while (index >= 0) {
current = current.next;
index--;
}
return current.val;
}
public void addAtHead(int val) {
ListNode temp = dummy.next; //先保存好下一个节点
ListNode newNode = new ListNode(val);
dummy.next = newNode;
newNode.next = temp;
size++;
}
public void addAtTail(int val) {
// 先指向尾部节点
ListNode current = dummy;
ListNode newNode = new ListNode(val);
while (current.next != null) {
current = current.next;
}
current.next = newNode;
size++;
}
public void addAtIndex(int index, int val) {
if (index < 0 || index > size) {
return;
}//注意这边index可以等于size,相当于addAtTail
ListNode current = dummy;
ListNode newNode = new ListNode(val);
// 注意这边应该是找到目标节点的前一个节点来进行操作
while (index > 0) {
current = current.next;
index--;
}
ListNode temp = current.next;
current.next = newNode;
newNode.next = temp;
size++;
}
public void deleteAtIndex(int index) {
if (index < 0 || index >= size) {
return;
}
ListNode current = dummy;
while (index > 0) {
current = current.next;
index--;
}
current.next = current.next.next;
size--;
}
}
LeetCode - 206. Reverse Linked List 翻转链表
利用前后两个指针来两两反转链表的指向,以下为双指针写法,pre起始为null是因为反转之后的head就成了尾节点,必须指向null,而原链表的尾节点最后被pre指针指向,因此最后只要返回pre即为反转后的结果。
class Solution {
public ListNode reverseList(ListNode head) {
if (head == null || head.next == null) return head;
ListNode pre = null;
ListNode cur = head;
while (cur != null) {
ListNode tempA = cur.next;
cur.next = pre;
pre = cur;
cur = tempA;
}
return pre;
}
}
反转列表的递归写法,其实逻辑和双指针是一样的,其要点就是下一层递归的时候参数cur和pre分别需要传入的是什么,只要明白了上面的双指针写法,递归的思路就更简单了。
class Solution {
//helper function
public ListNode reverse(ListNode cur, ListNode pre) {
if (cur == null) return pre; //终止条件
ListNode temp = cur.next;
cur.next = pre;
return reverse(temp, cur); // 相当于cur = temp; pre = cur;
}
public ListNode reverseList(ListNode head) {
return reverse(head, null);
}
}