链表经典问题之回文链表(c++、Java)力扣leetcode234

51 阅读8分钟

判断一个链表是否为回文链表。

方法一:快慢指针(无栈)

思路:

如果链表节点为偶数个,快指针指向null,慢指针在中间位置,后半段直接反转;

如果链表节点为奇数个,快指针指向最后一个节点,慢指针在中间位置,然后+1,后半段反转;

之后快指针移动到第一个节点,在逐个移动快慢指针,进行比较。

我盲写报错一堆,改了好久bug

以下是cpp的代码实现:(Java只需在此基础上改动一下语法就好,注释有写)

//redefinition of 'slow' 重复定义  ListNode* newslow=reserve(slow);才行


/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode() : val(0), next(nullptr) {}
 *     ListNode(int x) : val(x), next(nullptr) {}
 *     ListNode(int x, ListNode *next) : val(x), next(next) {}
 * };
 */
class Solution {
public:
    bool isPalindrome(ListNode* head) {
        ListNode *fast=head,*slow=head;//Java中把*去掉
        while(fast!=nullptr&&fast->next!=nullptr)//Java中->改为.
        {
            fast=fast->next->next;
            slow=slow->next;          
        }
        if(fast!=nullptr){
            slow=slow->next;
        }
        ListNode* newslow=reserve(slow);
        //slow=reserve(slow);在Java中可以,cpp中不行
        fast=head;
        while(newslow!=nullptr){
            if(fast->val!=newslow->val)
                return false;          
            fast=fast->next;
            newslow=newslow->next;          
        }
        return true;
    }

    ListNode *reserve(ListNode *head){//cpp中必须带*,Java中不带
        ListNode *pre=nullptr;
        while(head!=nullptr){
            ListNode *next=head->next;
            head->next=pre;
            pre=head;
            head=next;           
        }
        return pre;
    }
};

方法二:将值复制到数组中后用双指针

不推荐这种做法,规避了链表

class Solution {
public:
    bool isPalindrome(ListNode* head) {
        vector<int> vals;
        while (head != nullptr) {
            vals.emplace_back(head->val);
            head = head->next;
        }
        for (int i = 0, j = (int)vals.size() - 1; i < j; ++i, --j) {
            if (vals[i] != vals[j]) {
                return false;
            }
        }
        return true;
    }
};

方法三:递归

在递归解法中,我们可以使用快慢指针找到链表的中点,然后将中点之后的链表翻转。最后,我们分别从链表头和中点开始向后遍历,比较每个节点的值是否相同,来判断链表是否为回文链表。

以下是C++代码示例:

class Solution {
public:
    bool isPalindrome(ListNode* head) {
        if(head == nullptr) return true;
        ListNode* mid = findMid(head);
        ListNode* tail = reverseList(mid->next);
        ListNode* p = head;
        ListNode* q = tail;
        bool res = true;
        while(q != nullptr) {
            if(p->val != q->val) res = false;
            p = p->next;
            q = q->next;
        }
        mid->next = reverseList(tail);
        return res;
    }

    ListNode* findMid(ListNode* head) {
        ListNode* slow = head, * fast = head;
        while(fast->next != nullptr && fast->next->next != nullptr) {
            slow = slow->next;
            fast = fast->next->next;
        }
        return slow;
    }

    ListNode* reverseList(ListNode* head) {
        ListNode* prev = nullptr, * curr = head;
        while(curr != nullptr) {
            ListNode* next = curr->next;
            curr->next = prev;
            prev = curr;
            curr = next;
        }
        return prev;
    }
};

效率不错的方法。在力扣的判题中,这个代码时间上击败50%+的cpp用户。

方法四:栈(无快慢指针)

这个方法只需要使用栈来实现就可以了。

具体的思路是:将链表的每个节点压入栈中,然后再依次弹出节点进行比较,如果比较结果不相等,说明不是回文链表,否则就是回文链表。

【关于栈的小知识】

出栈:push

压栈:pop

栈是否为空:empty

栈的大小:size

访问栈顶:top

以下是cpp的代码实现:

bool isPalindrome(ListNode* head) {
    stack<int> s;
    ListNode* p = head;
    //把链表节点的值存放到栈中
    while (p != nullptr) {
        s.push(p->val);
        p = p->next;
    }
    //之后一边出栈,一边比较
    while (head != nullptr) {
        if (head->val != s.top()) {
            return false;
        }
        s.pop();
        head = head->next;
    }
    return true;
}
//这段代码的作用是判断给定的链表是否为回文链表。它先通过栈将链表中的每个节点的值按照原来的顺序存储下来,再通过依次比较节点的值和栈顶元素的值是否相等来判断是否为回文链表。

在力扣的判题中,这个代码时间上击败50%+的cpp用户,效率还可以。

这种方法的时间复杂度为O(n),空间复杂度为O(n),其中n为链表的长度。如果你的主要目标是判断链表是否为回文链表,那么这段代码可以胜任。不过如果空间复杂度有要求的话,可以考虑用双指针来优化这个算法。

记录我的一次失误:

在第二次while循环中,把head全部写成了p指针。

事实上p指针的用途只是压栈,不能用作二次 遍历。

Q:为什么在在第二次while循环中,要用head,而不用p指针?

**因为在第一个while循环中,我们已经将链表的元素通过指针p压入了栈中,此时p已经指向了链表的末尾,如果我们在第二个while循环中还继续使用指针p来遍历链表,那么p的值为nullptr,无法再遍历链表。

而head指针则不同,它在第一个while循环中指向链表的头部,此时我们可以使用head来遍历链表,同时也能够保证第一个while循环结束后head所指的位置为链表的尾部。

所以,第二个while循环中使用head指针遍历链表,可以避免指针p为空的情况。**

Q:为什么在在第二次while循环中,要用s.pop()。

因为我们在第一个while循环中,将链表节点的值一个一个存储到栈中,存储的顺序是反过来的,即最后一个节点的值被存储到了栈的顶部。而在第二个while循环中,我们需要将链表节点的值与栈中的值逐个进行比较,此时需要先将栈顶元素取出(也就是最后一个节点的值),再与链表当前节点的值进行比较,最后再将栈顶元素弹出。如果不使用s.pop()操作,则无法达到这个取出最后一个节点值的效果,比较结果也将会出错。 😊

以下是java的代码实现:

/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode() {}
 *     ListNode(int val) { this.val = val; }
 *     ListNode(int val, ListNode next) { this.val = val; this.next = next; }
 * }
 */
class Solution {
    public boolean isPalindrome(ListNode head) {
        ListNode temp = head;
        Stack<Integer> stack = new Stack();
        //把链表节点的值存放到栈中
        while (temp != null) {
            stack.push(temp.val);
            temp = temp.next;
        }
        //之后一边出栈,一边比较
        while (head != null) {
            if (head.val != stack.pop()) {
                return false;
            }
            head = head.next;
        }
        return true;
    }
}

在上述代码中,我们先将链表中的每个节点压入栈中,然后再依次弹出栈中的元素,与链表中的每个元素进行比较。如果比较结果不相同,则说明不是回文链表;否则就是回文链表。

方法五:栈+快慢指针

可以使用双指针和栈来实现判断链表是否为回文链表:

  1. 快慢指针找到链表中点,慢指针一次走一步,快指针一次走两步;
  2. 当快指针到达链表末尾时,慢指针刚好到达中点;
  3. 将慢指针后面的链表节点压入栈中;
  4. 从链表头开始遍历,同时从栈中弹出元素进行比较,如果不相同,则返回false,否则返回true。

下面是CPP的实现代码,其中ListNode是链表节点的结构体:

#include <stack>//C语言基础包里没有提供栈类型,cpp中提供了

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode() : val(0), next(nullptr) {}
 *     ListNode(int x) : val(x), next(nullptr) {}
 *     ListNode(int x, ListNode *next) : val(x), next(next) {}
 * };
 */
class Solution {
public:
    bool isPalindrome(ListNode* head) {
        if (!head || !head->next) return true; // 空链表或只有一个节点的链表为回文链表

        // 使用快慢指针找到链表中点
        ListNode* slow = head;
        ListNode* fast = head;
        while (fast && fast->next) {
            slow = slow->next;
            fast = fast->next->next;
        }

        // 将慢指针后面的链表节点压入栈中
        std::stack<ListNode*> s;
        while (slow) {
            s.push(slow);
            slow = slow->next;
        }

        // 从链表头开始遍历,同时从栈中弹出元素进行比较
        ListNode* cur = head;
        while (!s.empty()) {
            ListNode* top = s.top();
            s.pop();
            if (top->val != cur->val) {
                return false;
            }
            cur = cur->next;
        }

        return true;
        }
};

效率一般,击败了30%+的cpp用户

这两个栈的区别在于它们存储的数据类型不同。第一个栈 std::stack<ListNode*> s; 存储的是指向 ListNode 对象的指针,而第二个栈 stack s; 存储的是整型值。

在第一个栈中,每个元素都是一个指针,可以被用来指向链表中的某个节点。在第二个栈中,每个元素都是一个整型值,可以被用来存储任何整数。

由于这个区别,两个栈在具体使用上也会有所不同。例如,在实现一个基于栈的算法时,可以使用第一个栈来操作链表,而使用第二个栈来完成其他整型操作。

总结

方案一:栈

  1. 遍历链表,将链表的每个节点的值存入一个栈中;
  2. 再次遍历链表,比较链表节点的值和出栈的值是否相等,若有不等的则不是回文链表,否则是回文链表。

方案二:快慢指针 + 反转链表

  1. 使用快慢指针找到链表的中点,将链表分成两个部分;
  2. 反转链表的后半部分;
  3. 比较链表的前半部分和反转后的后半部分是否相等,若有不相等的则不是回文链表,否则是回文链表。

方案三:递归

  1. 使用快慢指针找到链表的中点,将链表分成两个部分;
  2. 递归到链表的尾部,依次向前比较节点的值是否相等,若有不相等的则不是回文链表,否则是回文链表。

方案四:数组

  1. 将链表的节点值存入数组中;
  2. 使用双指针从数组的两端向中间依次比较是否相等,若有不相等的则不是回文链表,否则是回文链表。

其余方案均为这四种的变形和结合。

同时,这里附上LeetCode上该问题的链接:leetcode-cn.com/problems/pa…