19. 删除链表的倒数第N个结点
首先, 这个链表是一个单项链表, 其次, 需要逆序删除结点,
我首先想的, 能不能递归? 要不然就用一个栈存所有, 最后出栈解决问题,
这样想来, 用堆栈解决应该是比较正常的
那么, 首先用栈, 虽然很拉胯, 但是用用嘛
嗯, 还是去看解法思路了, 本来只是想看个标题就跑, 但是只看标题还是不明白具体实现
-
递归, 真的有人用递归做出来了, 说明是可行的, 那我的看看怎么做到
那么, 让我来梳理一下关键词, 链表, 递归, 计数
同时要注意, 递归是同样用到栈操作的, 你是不是注意到了什么, 想到什么了, 说说, 压到栈里, 说明递归到最后, 结束的标志是 node.next == null 这点没法反驳对吧, 所以这里才是关键, 达到这个标志就是最后的地方,从这里, 开始从0计数, 达到n咱们就跑路, 但是还是有问题, 计数是到了节点, 只是刚刚好的节点, 但是如何删除这个节点呢
2022年8月19日15:03 更新, 发现计数的位置刚好是走到要删除节点的前一个位置, 是我疏忽了
所以需要从最开始的角度来思考, 判断, 如果只有一个的情况下, 要删除一个怎么办, 其他的, 就找它的前一个节点进行删除
我们先来梳理一下链表的结构
那么, 如果是单一节点, 删除一个
只要直接null就好
如果是两个节点及以上, 就用.next = null, 如果是头结点另行处理,
但是自己的处理结果, 怎么都不会觉得满意, 不仅繁杂, 而且, 没法处理头结点的情况,
一开始, 我的处理方式是这样
// 统一, 一定需要头指针进行比对操作
public static int removeNode(ListNode head, ListNode node, int n) {
// 特殊情况:倒数第一个
if (node.next == null) {
// 同时, 如果刚好删除倒数第一个结点,
if (n == 1) {
// 同时这个节点就是头指针
if (node == head) {
head = null;
}
node = null;
}
return 1;
}else{
int m = removeNode(head,node.next,n);
if(m == n){
if(n == 1){
node.next = null;
}else{
node.next = node.next.next;
}
}
// 针对删除头结点的情况进行特殊处理, 由于到头节点的情况下不能再递归, 那我就手动处理, 反正到了头结点我也不再需要额外内容的了
if(node == head) {
if ((m + 1) == n) {
node = node.next;
head = node;
}
}
return m+1;
}
}
不仅需要头指针, 而且每次遍历还需要判断, 最重要的的是, 我这套思想是照着之前数据结构里边C语言来实现的, 在指针情况下, 我修改head没有问题, 但是, 这里, 是! Java! 传进来的Head你居然想改它让它传出去, 这是一个非常不好的想法, 传不出去, 同时在递归
所以, 根据大佬的思想, 我重新构建了一下代码, 这个是评论区大佬的递归方法, 本来想标注一下名字的, 但是过了一天回去找, 发现评论区那边翻不到了, 有点尴尬
确实,优雅
/*
* @param null:
* @return
* @author/作者 TenderFlow
* @description/描述 Tip
* @date/时间 2022/8/17 10:08
*/
public static ListNode removeNthFromEnd1(ListNode head, int n) {
// 递归如何做到倒数
int result = removeNode(head, n);
// 我们会发现, 在里边只是处理了非头结点的问题, 那么头结点, 不就在这里吗, 我们只需要在这里,完成递归的最后一项, 太优雅了
if(result == n){
return head.next;
}
return head;
}
// 处理删除非头结点的情况
public static int removeNode(ListNode node, int n) {
if (node == null){
return 0;
}
int m = removeNode(node.next,n);
if(m == n){
node.next = node.next.next;
}
return m+1;
}
真的是, 完美解法, 我悟了
接下来就是解法2, 最难的解法我都解决了, 我已经全部理解了(叉腰),蛐蛐双指针, 何足挂齿
-
双指针, 是大名鼎鼎的双指针哇, 确实也是很好使的东西
确实, 好写很多, 整体写下来, 对于逻辑冲突的解决是比较流畅的
原理就是两个节点对象(ListNode),一个先沿着链表走n步, 走到以后另一个才开始走, 这样最先走的一个走到倒数第一个了, 第二个节点对象就在要删除的内容附近了(可能就是, 可能在要删除的节点前一个, 这个具体还是看我们一开始走的实现上可能有区别, 一般是在要删除节点的前一个节点停下来), 也是很优雅的写法, 哦吼吼吼, 感觉自己也变得优雅了
/*
* @param null:
* @return
* @author/作者 TenderFlow
* @description/描述 Tip
* @date/时间 2022/8/17 17:05:23
*/
public static ListNode removeNthFromEnd1(ListNode head, int n) {
ListNode nodeFast = head;
ListNode nodeLow = head;
for (int i = 0; i < n; i++) {
nodeFast = nodeFast.next;
}
if (nodeFast == null) {
return head.next;
} else {
while (nodeFast.next != null) {
nodeFast = nodeFast.next;
nodeLow = nodeLow.next;
}
nodeLow.next = nodeLow.next.next;
return head;
}
}
总结:
-
可以使用栈, 但是栈和递归在理念上是完全相同的, 两者基本是差不多的
- 使用双指针, 前后两个指针差距几个节点, 然后向一把尺子移动到终点, 那么尺子的起点就是我们需要删除的节点, 不过是处理删除节点的时候有些问题罢了, 其他, 都没什么好说的
- 是否需要使用哑节点,哑节点只是为了简便头节点的删除工作, 但是实际上看起来, 头结点的删除其实并不难, 头结点处理的难处是为了替换掉原先的指针, 在一个函数内, 但是, 对于真正熟悉指针的人来说, 这个其实并不是非常难以理解和处理的问题,因为如果使用返回值的话, 我们想怎么处理就怎么处理, 确实好使, 以前被void折磨太多次了, 反而被束缚了手脚
方法3: 最简单的傻瓜式方法
先遍历一遍找长度, 再遍历一遍找节点, 让我比较奇怪的是, 这个方法在测试的时候反而效果好,大概是因为数据量其实并不大的缘故吧, 大家都是0ms,而这种方式又把递归的堆栈所需的空间节省了, 所以看起来比较快, 那么, 我的兴趣就来了, 既然大家都是最快, 都是时间击败100%的人, 我可不可以, 在空间上击败100%的人呢?
/*
* @param null:
* @return
* @author/作者 TenderFlow
* @description/描述 Tip
* @date/时间 2022/8/19 15:25
*/
public ListNode removeNthFromEnd1(ListNode head, int n) {
int len = 0;
ListNode cur = head;
// 获取长度
while (cur != null){
cur = cur.next;
len = len+1;
}
if(len == 1 && n == 1) return null;
if(len == n) return head.next;
cur = head;
int cnt = 0; //计数
while (cnt < len-n-1){
cur = cur.next;
cnt++;
}
if(n == 1) cur.next = null;
else cur.next = cur.next.next;
return head;
}
但是一试我就知道我错了, 我的代码相比对方甚至经过简化, 但是内存使用大小比他大, 完全无法理解, 甚至使用了一下相同的代码, 我的提交记录相比两天前内存消耗量是增加的, 这东西也看玄学的吗? 无法理解, 不管了, 梦碎了, 咱开摆

浙公网安备 33010602011771号