java链表奇数倒序偶数顺序_高频面试系列/判断回文链表.md · 黑桃/fucking-algorithm - Gitee.com...

我们之前有两篇文章写了回文串和回文序列相关的问题。

**寻找**回文串的核心思想是从中心向两端扩展:

```cpp

string palindrome(string& s, int l, int r) {

// 防止索引越界

while (l >= 0 && r < s.size()

&& s[l] == s[r]) {

// 向两边展开

l--; r++;

}

// 返回以 s[l] 和 s[r] 为中心的最长回文串

return s.substr(l + 1, r - l - 1);

}

```

因为回文串长度可能为奇数也可能是偶数,长度为奇数时只存在一个中心点,而长度为偶数时存在两个中心点,所以上面这个函数需要传入`l`和`r`。

而**判断**一个字符串是不是回文串就简单很多,不需要考虑奇偶情况,只需要「双指针技巧」,从两端向中间逼近即可:

```cpp

bool isPalindrome(string s) {

int left = 0, right = s.length - 1;

while (left < right) {

if (s[left] != s[right])

return false;

left++; right--;

}

return true;

}

```

以上代码很好理解吧,**因为回文串是对称的,所以正着读和倒着读应该是一样的,这一特点是解决回文串问题的关键**。

下面扩展这一最简单的情况,来解决:如何判断一个「单链表」是不是回文。

### 一、判断回文单链表

输入一个单链表的头结点,判断这个链表中的数字是不是回文:

```java

/**

* 单链表节点的定义:

* public class ListNode {

* int val;

* ListNode next;

* }

*/

boolean isPalindrome(ListNode head);

输入: 1->2->null

输出: false

输入: 1->2->2->1->null

输出: true

```

这道题的关键在于,单链表无法倒着遍历,无法使用双指针技巧。那么最简单的办法就是,把原始链表反转存入一条新的链表,然后比较这两条链表是否相同。关于如何反转链表,可以参见前文「递归操作链表」。

其实,**借助二叉树后序遍历的思路,不需要显式反转原始链表也可以倒序遍历链表**,下面来具体聊聊。

对于二叉树的几种遍历方式,我们再熟悉不过了:

```java

void traverse(TreeNode root) {

// 前序遍历代码

traverse(root.left);

// 中序遍历代码

traverse(root.right);

// 后序遍历代码

}

```

在「学习数据结构的框架思维」中说过,链表兼具递归结构,树结构不过是链表的衍生。那么,**链表其实也可以有前序遍历和后序遍历**:

```java

void traverse(ListNode head) {

// 前序遍历代码

traverse(head.next);

// 后序遍历代码

}

```

这个框架有什么指导意义呢?如果我想正序打印链表中的`val`值,可以在前序遍历位置写代码;反之,如果想倒序遍历链表,就可以在后序遍历位置操作:

```java

/* 倒序打印单链表中的元素值 */

void traverse(ListNode head) {

if (head == null) return;

traverse(head.next);

// 后序遍历代码

print(head.val);

}

```

说到这了,其实可以稍作修改,模仿双指针实现回文判断的功能:

```java

// 左侧指针

ListNode left;

boolean isPalindrome(ListNode head) {

left = head;

return traverse(head);

}

boolean traverse(ListNode right) {

if (right == null) return true;

boolean res = traverse(right.next);

// 后序遍历代码

res = res && (right.val == left.val);

left = left.next;

return res;

}

```

这么做的核心逻辑是什么呢?**实际上就是把链表节点放入一个栈,然后再拿出来,这时候元素顺序就是反的**,只不过我们利用的是递归函数的堆栈而已。

![](../pictures/回文链表/1.gif)

当然,无论造一条反转链表还是利用后续遍历,算法的时间和空间复杂度都是 O(N)。下面我们想想,能不能不用额外的空间,解决这个问题呢?

### 二、优化空间复杂度

更好的思路是这样的:

**1、先通过「双指针技巧」中的快慢指针来找到链表的中点**:

```java

ListNode slow, fast;

slow = fast = head;

while (fast != null && fast.next != null) {

slow = slow.next;

fast = fast.next.next;

}

// slow 指针现在指向链表中点

```

![](../pictures/回文链表/1.jpg)

**2、如果`fast`指针没有指向`null`,说明链表长度为奇数,`slow`还要再前进一步**:

```java

if (fast != null)

slow = slow.next;

```

![](../pictures/回文链表/2.jpg)

**3、从`slow`开始反转后面的链表,现在就可以开始比较回文串了**:

```java

ListNode left = head;

ListNode right = reverse(slow);

while (right != null) {

if (left.val != right.val)

return false;

left = left.next;

right = right.next;

}

return true;

```

![](../pictures/回文链表/3.jpg)

至此,把上面 3 段代码合在一起就高效地解决这个问题了,其中`reverse`函数很容易实现:

```java

ListNode reverse(ListNode head) {

ListNode pre = null, cur = head;

while (cur != null) {

ListNode next = cur.next;

cur.next = pre;

pre = cur;

cur = next;

}

return pre;

}

```

![](../pictures/kgroup/8.gif)

算法总体的时间复杂度 O(N),空间复杂度 O(1),已经是最优的了。

我知道肯定有读者会问:这种解法虽然高效,但破坏了输入链表的原始结构,能不能避免这个瑕疵呢?

其实这个问题很好解决,关键在于得到`p, q`这两个指针位置:

![](../pictures/回文链表/4.jpg)

这样,只要在函数 return 之前加一段代码即可恢复原先链表顺序:

```java

p.next = reverse(q);

```

篇幅所限,我就不写了,读者可以自己尝试一下。

### 三、最后总结

首先,寻找回文串是从中间向两端扩展,判断回文串是从两端向中间收缩。对于单链表,无法直接倒序遍历,可以造一条新的反转链表,可以利用链表的后序遍历,也可以用栈结构倒序处理单链表。

具体到回文链表的判断问题,由于回文的特殊性,可以不完全反转链表,而是仅仅反转部分链表,将空间复杂度降到 O(1)。

坚持原创高质量文章,致力于把算法问题讲清楚,欢迎关注我的公众号 labuladong 获取最新文章:

![labuladong](../pictures/labuladong.jpg)

[上一篇:如何寻找缺失和重复的元素](../高频面试系列/缺失和重复的元素.md)

[下一篇:如何在无限序列中随机抽取元素](../高频面试系列/水塘抽样.md)

[目录](../README.md#目录)

一键复制

编辑

Web IDE

原始数据

按行查看

历史

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
双向链表是一种常见的数据结构,与单向链表相比,它可以在节点之间进行双向遍历。在Java中,我们可以使用类来实现双向链表。 下面是一个简单的Java程序,演示如何创建和输出双向链表: ```java // 双向链表节点类 class Node { public int data; public Node prev; public Node next; public Node(int data) { this.data = data; this.prev = null; this.next = null; } } // 双向链表类 class DoubleLinkedList { public Node head; public Node tail; public DoubleLinkedList() { this.head = null; this.tail = null; } // 在链表头部插入节点 public void insertAtHead(int data) { Node newNode = new Node(data); if (head == null) { head = newNode; tail = newNode; return; } newNode.next = head; head.prev = newNode; head = newNode; } // 在链表尾部插入节点 public void insertAtTail(int data) { Node newNode = new Node(data); if (tail == null) { head = newNode; tail = newNode; return; } newNode.prev = tail; tail.next = newNode; tail = newNode; } // 输出链表 public void printList() { Node current = head; while (current != null) { System.out.print(current.data + " "); current = current.next; } System.out.println(); } } // 测试类 public class Main { public static void main(String[] args) { DoubleLinkedList list = new DoubleLinkedList(); list.insertAtHead(1); list.insertAtHead(2); list.insertAtTail(3); list.insertAtTail(4); list.printList(); // 输出:2 1 3 4 } } ``` 在上面的程序中,我们首先定义了一个`Node`类来表示双向链表的每个节点,其中包含了当前节点的值、前一个节点和后一个节点。接着,我们定义了`DoubleLinkedList`类来表示整个双向链表,其中包含了头节点和尾节点。 在`DoubleLinkedList`类中,我们定义了`insertAtHead`和`insertAtTail`方法来在链表头部和尾部插入节点,分别需要创建一个新节点,并将它与当前链表的头节点或尾节点进行连接。 最后,我们定义了`printList`方法来输出整个链表,只需要从链表的头节点开始遍历,依次输出每个节点的值即可。 在`Main`类中,我们通过`DoubleLinkedList`类创建了一个双向链表,并向其中插入了四个节点。最后,我们调用`printList`方法输出整个链表

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值