链表理论基础
链表的结构类似于一串珠子,每一颗珠子就相当于链表上的一个节点;每一个节点则由数据域和指针域构成,数据域用于存放数据,指针域用于指向其它一个节点或空节点(链表尾部)。
链表与数组的主要区别在于:
- 数组是在内存中是连续分布的,但是
链表在内存中不是连续分布的
。 - 数组在定义的时候,长度就是固定的,如果想改动数组的长度,就需要重新定义一个新的数组。
链表的长度是不固定的
,可以进行动态增删, 适合数据量不固定,频繁增删,较少查询的场景。
更多有关链表的理论基础可查阅:《代码随想录》链表理论基础
203.移除链表元素
题目详细:LeetCode.203
在Java中,想要对链表中某一节点进行移除操作,只要将被移除节点的前一个节点的next指针域,指向被移除节点的下一个节点。
由题可知,输入的链表是一个单链表,所以我们需要:
- 定义一个虚拟头节点,其指针域指向链表头head,在定义时传递的head是值传递,所以后面我们对head进行任何操作都不会影响该节点始终指向链表头。
- 定义一个指针始终指向当前节点的上一个节点,才能在找到被移除节点时,将上一个节点的指针域指向被移除节点的下一个节点,以此来完成移除链表元素的操作。
class Solution {
public ListNode removeElements(ListNode head, int val) {
ListNode ans = new ListNode(0, head);
// 这里多定义了一个节点指针p来遍历链表,可以提高代码的可阅读性。
ListNode p = head, pre = ans;
while(p != null){
if(p.val == val){
pre.next = p.next;
}else{
pre = p;
}
p = p.next;
}
return ans.next;
}
}
- 直接更新head来对链表进行遍历也是可行的,但如果对链表不是很熟悉的话,在遇到复杂的问题情境时,代码量一多就很容易会被这个变量名搞混。
- 所以在这道题中,我
额外定义了一个节点指针p来遍历链表,而不是直接更新head来对链表进行遍历,可以提高代码的可阅读性
,方便理解。
707.设计链表
题目详细:LeetCode.707
【太忙了,后续再补上】
206.反转链表
题目详细:LeetCode.206
看到反转相关的题目,我脑袋中第一个蹦出来的思路就是利用栈来解题,利用栈先进后出(FILO)的特点,能够非常好理解地解决这道题目。
- 定义一个辅助栈,依次遍历原链表的每个节点并依次进栈
- 遍历完后,节点再依次出栈,并重新赋值指针域,使其指向下一个出栈的节点
- 当栈空时,即所有节点都已出栈,返回头节点即可得到反转后的链表。
class Solution {
public ListNode reverseList(ListNode head) {
Stack<ListNode> stack = new Stack<>();
while(head != null){
stack.push(head);
head = head.next;
}
ListNode ans = new ListNode(-1);
ListNode p = ans;
while(!stack.isEmpty()){
p.next = stack.pop();
p = p.next;
p.next = null;
}
return ans.next;
}
}
- 通过上述代码可以发现,整个链表的反转结果可以视作由一个个小节点逐一反转得到的结果,即一个问题的结果是由一个个子问题的结果得到的,这就让我联想到了递归算法;
- 在编程语言中使用函数自调用,可以非常方便的实现递归过程,其帮助我们实现了一个栈,每一次调用自身函数时则进行一次压栈操作;
- 递归和循环迭代算法两者也是互通的,所有的递归程序或算法都能转化为迭代程序或算法,所以递归操作必须要有一个停止条件,否则将一直递归直到栈溢出而报错;
- 当满足停止条件时,则开始逐一出栈,执行子问题后续的代码,并将子问题的结果返回给下一个出栈的子问题处理,直到最后一个栈底问题执行完毕并得到的结果即为预期结果。
所以当我们解决一个问题用到栈的时候,也可以尝试使用递归来解决:
class Solution {
public ListNode reverseList(ListNode head) {
// 递归停止条件
if(head == null || head.next == null){
return head;
}
// 函数自调用,压栈
ListNode ans = reverseList(head.next);
head.next.next = head;
head.next = null;
return ans;
}
}
链表啊链表啊,感觉是比较聊胜于无的东西吧,但是呢其实在实际应用中并不是很常用到,能用数组解决的问题就几乎不参用链表,虽然链表从性能上与数组做比较,可以发现:
- 链表的删除和插入操作的时间复杂度是 O(1)
- 链表的查找操作的时间复杂度是 O(n)
但是在实际应用时,我们删除某一节点的数据,或者在某个非特定的位置(如非表头、非表尾)插入数据节点时,都需要先进行查找操作,所以从宏观上来讲,其删除和插入操作的时间复杂度也是 O(n),是很得不偿失的。
这里引用链表的发明者的一句话:
“我创造了它(链表结构),但我几乎没使用它。”
今日作结诗,同时也是一点领悟:
不为无益之事,何以遣此有涯之生。
“不做看起来好像没有意义的事,怎么度过这漫长又有限的生命旅程。”