目录
一、链表的使用:LeetCode例题
删除链表中等于给定值val的所有元素。
示例:给定 1->2->6->3->4->5->6,val = 6;返回:1->2->3->4->5。
ListNode为题中给出的链表代码
public class ListNode {
public int val;
public ListNode next;
public ListNode(int x){
val = x;
}
}
1、解法一:不使用虚拟头节点
public ListNode removeElements(ListNode head, int val) {
// 循环遍历头节点
while (head != null && head.val == val) {
ListNode delNode = head;
// 绕过删除节点,链接转移
head = head.next;
delNode.next = null;
}
if (head == null) {
return null;
}
// 循环头节点以外的节点
ListNode prev = head;
while (prev.next != null) {
if (prev.next.val == val) {
prev.next = prev.next.next;
} else {
// 循环到下一个
prev = prev.next;
}
}
return head;
}
2、解法二:使用虚拟头节点
使用虚拟头节点,可以统一链表的操作,简化实现逻辑。
public ListNode removeElements(ListNode head, int val) {
// 设立虚拟头节点
ListNode dummyHead = new ListNode(-1);
dummyHead.next = head;
// 设立虚拟头节点以后,就不需要再另外处理头节点了
// 以为,从虚拟头节点开始遍历,此时每一个有效的节点都有前一个节点
ListNode prev = dummyHead;
while (prev.next != null) {
if (prev.next.val == val) {
ListNode delNode = prev.next;
prev.next = delNode.next;
} else {
// 循环到下一个
prev = prev.next;
}
}
return dummyHead.next;
}
为了能在本地进行测试,重新编写了一下ListNode的相关代码,自定义了ListNode构造函数,代码入下:
public class ListNode {
public int val;
public ListNode next;
public ListNode(int x){
val = x;
}
// 链表节点的构造函数
// 使用arr作为参数,创建一个链表,当前的ListNode为链表头节点
public ListNode(int[] arr){
if(arr == null && arr.length == 0){
throw new IllegalArgumentException("arr cannot be empty.");
}
// 头节点
this.val = arr[0];
ListNode cur = this;
for (int i = 1; i < arr.length; i++) {
cur.next = new ListNode(arr[i]);
// 不断循环赋值
cur = cur.next;
}
}
@Override
public String toString(){
StringBuilder sb = new StringBuilder();
ListNode cur = this;
while(cur != null){
sb.append(cur.val + "->");
// 循环移动
cur = cur.next;
}
// 表示已经到达了链表末尾
sb.append("NULL");
return sb.toString();
}
}
二、递归
递归的本质就是把原来的问题转化为更小的同一问题
理解递归:本质无非是一个函数里边调用了另一个函数,只是所调用的函数是他本身而已。
举例:数组求和
1、链表的天然递归性
如下是一个链表的基本构成图示:
我们如果用递归的视角来看它,就是一个节点链接了一个更短的链表,如下:
利用上边递归的思想,我们重新来实现上边leetcode的例题,示例代码如下:
public class Solution {
public ListNode removeElements(ListNode head, int val) {
// 从头节点开始判断起
if(head == null){
return null;
}
// 除头节点的外,更短的链表
ListNode res = removeElements(head.next,val);
if(head.val == val){
// 如果是删除元素,丢弃头节点
return res;
}else{
// 如果非删除元素,头节点保留
head.next = res;
return head;
}
}
}
进一步简化为:
public class Solution {
public ListNode removeElements(ListNode head, int val) {
// 从头节点开始判断起
if(head == null){
return null;
}
// 除头节点的外的,更短的链表
head.next = removeElements(head.next,val);
return head.val == val ? head.next : head;
}
}
使用递归的核心思想是:使用基本问题的解去构建整体问题的解。图示解如下:
2、递归运行机制
递归的运行机制,还是使用基本问题的解去构建整体问题的解,下边为了更加清楚的描述递归的运行机制,使用一个数组求和的简单例子来做步骤分解:
求解:arr = [6,10] 数组元素的和,过程示例如下,n为数组的长度,此处为简写
上图示例的过程可以理解为:0 + 10 + 6;
1、遍历到数组最后一个元素(l = n = 2),没有相加的数据,所以函数执行返回0;
2、在上一个函数求解返回之后,函数在停顿后继续执行,得到上一个调用计算的值 x = 0 ; 那么 res = arr[1] + 0;即res = 10;
3、当前一个函数执行完后,得到计算的值 x = 10;函数继续执行,那么 res = arr[0] + 10;所以 res = 16;至此,整个函数执行结束。
注意:递归调用是有代价的 ——> 函数调用+系统栈空间(递归深度)