[OJ]数据结构:链表回文

目录

题目来源:牛客网(NewCoder)

一、解题思路

二、完整代码

三、图解

1.初始化

2.获取中间节点

3.反转后链表

4.比较链表

四、常见错误与注意事项

1.边界处理

2.快慢指针终止条件

3.链表断开时空指针问题

4.比较的终止信号

五、复杂度分析

六、总结


题目来源:牛客网(NewCoder)

        对于一个链表,请设计一个时间复杂度为O(n),额外空间复杂度为O(1)的算法,判断其是否为回文结构。给定一个链表的头指针A,请返回一个bool值,代表其是否为回文结构。保证链表长度小于等于900。

测试样例:

        1->2->2->1

        返回:true

一、解题思路

什么是回文链表?

回文链表指的是链表节点值从前往后读和从后往前读完全一致,例如:

  • 1->2->2->1

  • 1->2->3->2->1

  • 1(单个节点)

  • 空链表

对于这种题型,我们可以通过快慢指针+链表反转进行解决,主要分为以下三步:

  1. 利用快慢指针找到链表的中间节点,如何获取,可以参考另一篇博客:链表的中间节点
  2. 将链表的后半部分原地反转,关于反转的方式,可以参考:反转链表
  3. 同时遍历链表的前半部分和后半部分,比较节点值。

二、完整代码

/*
struct ListNode {
    int val;
    struct ListNode *next;
    ListNode(int x) : val(x), next(NULL) {}
};
*/

typedef struct ListNode Node;

class PalindromeList {
public:
    bool chkPalindrome(ListNode* A) {
        // 空链表或单个节点链表都是回文
        if (A == NULL || A->next == NULL) 
        {
            return true;
        }
        
        //使用快慢指针找到链表中点
        Node* fast = A;
        Node* slow = A;
        Node* prev = NULL;
        
        // 快指针每次走两步,慢指针每次走一步
        // 当快指针到达链表末尾时,慢指针正好到达中点(或中点前一个)
        while (fast != NULL && fast->next != NULL) 
        {
            prev = slow;  // 记录慢指针的前一个节点
            fast = fast->next->next;
            slow = slow->next;
        }
        
        // 将链表从中间断开,分为前后两部分
        // 注意:prev可能是NULL(当链表只有两个节点时)
        if (prev != NULL) 
        {
            prev->next = NULL;
        }
        
        // 反转后半部分链表
        Node* l1 = NULL;     // 反转后新链表的头节点
        Node* l2 = slow;     // 当前待反转的节点
        Node* l3 = NULL;     // 临时保存下一个节点
        
        while (l2 != NULL) 
        {
            l3 = l2->next;   // 保存下一个节点
            l2->next = l1;   // 当前节点指向前一个节点
            l1 = l2;         // 更新新链表的头节点
            l2 = l3;         // 移动到下一个待反转节点
        }
        
        // 此时l1是反转后的后半部分链表的头节点
        slow = l1;
        
        // 比较前后两部分链表
        Node* front = A;
        
        while (front != NULL && slow  != NULL) 
        {
            if (front->val != slow ->val) 
            {
                return false;  // 发现不匹配,不是回文
            }
            front = front->next;
            slow = slow ->next;
        }
        
        // 注意:链表长度可能是奇数,此时后半部分会比前半部分多一个节点
        // 但这个多出来的节点是原链表的中间节点,不需要比较
        
        return true;  // 所有节点都匹配,是回文
    }
};

三、图解

假设有链表A:

1.初始化

2.获取中间节点

fast指针每次向后走两步,slow指针每次往后走一步,当fast指针到达最后一个节点或者NULL到达NULL时,slow正好落于链表的中间节点,prev指针指向前链表的末端。

fast = NULL || fast->next = NULL;

此时:
prev = 节点2,正好是前链表的最后一个节点,需要将其next指向NULL;
slow作为后一个链表反转的初始头(后面将是尾);

3.反转后链表

参考反转链表的实现,我们可以得到以下新链表

至此,我们已经获取了两个链表,分别为前链表和后链表

4.比较链表

        这里为了避免更改原链表头指针,我们可以将slow指针重新指向后一个链表的头节点,然后创建一个指针指向前链表的头节点。

比较过程:
1. front=1, back=1 → 相等
2. front=2, back=2 → 相等
3. front=NULL, back=3 → 前半部分结束

虽然后半部分还有节点3,但这是原链表的中间节点,不需要比较,所以链表是回文结构

四、常见错误与注意事项

1.边界处理

// 错误:没有处理空链表或单节点链表
// 正确做法:
if (A == NULL || A->next == NULL) 
{
    return true;
}

2.快慢指针终止条件

// 错误:只检查fast->next,可能访问空指针
while (fast->next) // 如果fast为NULL,会访问空指针的next
{  
    fast = fast->next->next;
    slow = slow->next;
}

// 正确:同时检查fast和fast->next
while (fast != NULL && fast->next != NULL) 
{
    fast = fast->next->next;
    slow = slow->next;
}

3.链表断开时空指针问题

// 错误:假设prev总是非空
prev->next = NULL;  // 如果链表只有两个节点,prev可能是NULL

// 正确:检查prev是否为空
if (prev != NULL) 
{
    prev->next = NULL;
}

4.比较的终止信号

// 错误:只比较到其中一个链表结束
while (front != NULL) // 如果后半部分比前半部分短,会访问空指针
{  
    if (front->val != slow->val) 
    {
        return false;
    }
    front = front->next;
    slow= slow->next;
}

// 正确:两个链表都要检查
while (front != NULL && slow!= NULL) 
{
    if (front->val != slow->val) 
    {
        return false;
    }
    front = front->next;
    slow= slow->next;
}

五、复杂度分析

时间复杂度:O(n)

        O(n) + O(n) + O(n) = O(3n) = O(n)

空间复杂度:O(1)

六、总结

        通过快指针每次走两步、慢指针每次走一步的方式,在单次遍历中找到链表中点。将链表的后半部分原地反转,这样我们就可以从两端向中间比较。同时遍历前半部分和反转后的后半部分,比较对应位置的节点值。

        虽然需要多次遍历链表,但每次遍历都是O(n)时间复杂度。

<think>嗯,用户想了解在线评测系统(OJ)中常用的Java数据结构,特别是编程竞赛相关的。首先,我需要回忆一下OJ系统的特点和编程竞赛中的常见问题类型。通常OJ题目会涉及算法效率,所以数据结构的选择很关键。 首先,基础的数据结构肯定是必须的,比如数组、字符串这些。Java里的数组和String类用得很多,特别是StringBuilder在处理字符串拼接时效率更高,避免频繁创建对象。这点应该要提到。 然后是集合框架里的List,比如ArrayList和LinkedList。ArrayList的随机访问快,适合查找多的场景,而LinkedList在频繁插入删除时更好,比如链表相关的题目。不过实际编程竞赛中可能更多用ArrayList,因为大部分情况下访问更频繁。 队列和栈在OJ题里也很常见,比如BFS用队列,DFS用栈或者递归。Java中的Queue可以用LinkedList实现,还有Deque双端队列,可能用来做滑动窗口或者双向BFS。栈的话一般用Deque来模拟,因为Java的Stack类已经不推荐使用了。 接下来是Set和Map,HashSet和HashMap用于快速查找和去重,TreeSet和TreeMap在需要有序的情况下使用。比如判断元素是否存在,或者统计频率,这些结构很常用。不过要注意哈希表的冲突问题,可能会影响性能,但在编程竞赛中数据规模通常可控。 优先队列,也就是堆,PriorityQueue,处理贪心算法或者Top K问题,比如Dijkstra算法里会用到。需要提醒用户注意比较器的使用,自定义排序规则。 高级数据结构比如并查集,处理连通性问题,虽然Java标准库没有内置,但可以自己实现。还有树状数组和线段树,用于区间查询和更新,这些在处理区间问题时很高效,比如区间求和、最大值等。 字符串处理相关的字典树,可能用于前缀匹配,不过实际题目中可能相对较少,但如果有的话需要自己实现节点结构。 然后,用户可能想知道在OJ中使用这些数据结构的注意事项。比如输入输出效率,Java的Scanner比较慢,用BufferedReader更好。内存管理方面,避免频繁创建对象,防止超限。还有数据规模的问题,根据题目选择合适复杂度的结构。 最后,可能需要给出一些例子,比如用ArrayList处理动态数组,HashMap统计频率,PriorityQueue解决Top K问题,并查集处理朋友圈问题等。这些例子能帮助用户更好地理解应用场景。 需要确保回答的结构清晰,分点列出常用结构,每个结构的特性和应用场景,以及注意事项。同时要引用提供的资料,比如引用[1]提到OJ系统涵盖数据结构等核心知识点,引用[2]关于Java的编译处理,可能涉及数据结构的实现。可能还要提到引用[3]中的私有训练题目提交情况,但不确定是否相关,可能不需要。 检查是否有遗漏的结构,比如图结构通常用邻接表或矩阵表示,但Java中可以用List<List<Integer>>来表示邻接表。另外,位操作相关的结构比如BitSet,可能在处理位掩码时有用,不过可能属于较高级的应用,根据用户的问题层次决定是否包含。 总之,整理这些数据结构,分基础、集合框架、高级结构,注意事项,示例应用,相关问题,确保覆盖全面,符合用户需求。</think>### 在线评测系统(OJ)中常用的Java数据结构 在线评测系统(OJ)的编程竞赛题目通常涉及高效算法实现,而数据结构的选择直接影响代码性能和解题效率。以下是Java中常用的数据结构及其应用场景: --- #### 一、基础数据结构 1. **数组(Array)** - 特性:固定大小,内存连续,支持随机访问($O(1)$时间复杂度) - 应用:静态数据存储、矩阵运算、动态规划 - Java实现:`int[] arr = new int[10];` 2. **字符串(String)** - 特性:不可变性,适合频繁读取但需注意拼接效率 - 优化:使用`StringBuilder`或`StringBuffer`进行动态修改($O(n)$拼接复杂度) - 示例:回文判断、子串匹配 --- #### 二、集合框架核心类 3. **List接口** - **`ArrayList`** - 动态数组,支持快速随机访问($O(1)$),插入/删除尾部高效($O(1)$),中间操作低效($O(n)$) - 应用:需要频繁按索引访问的场景 - **`LinkedList`** - 双向链表,插入/删除高效($O(1)$),但随机访问慢($O(n)$) - 应用:队列、栈的实现,或需要频繁中间修改的场景 4. **Queue/Deque接口** - **`LinkedList`实现队列** - 示例:BFS(广度优先搜索)中的层级遍历 - **`PriorityQueue`** - 基于堆实现,支持$O(\log n)$插入和$O(1)$获取最值 - 应用:贪心算法、Top K问题 - **`ArrayDeque`** - 双端队列,适合滑动窗口或双向BFS 5. **Set接口** - **`HashSet`** - 哈希表实现,$O(1)$查找/插入,用于去重或快速存在性判断 - **`TreeSet`** - 红黑树实现,有序集合,支持$O(\log n)$查找和范围查询 6. **Map接口** - **`HashMap`** - 哈希表存储键值对,$O(1)$时间复杂度,适合频率统计、缓存 - **`TreeMap`** - 基于红黑树,键有序,支持范围查询(如第一个大于某值的键) --- #### 三、高级数据结构(需手动实现) 7. **并查集(Disjoint Set Union, DSU)** - 功能:高效处理连通性问题($O(\alpha(n))$近似常数时间) - 应用:图的连通分量、朋友圈问题 - 示例代码: ```java class DSU { int[] parent; public DSU(int n) { parent = new int[n]; Arrays.setAll(parent, i -> i); } int find(int x) { return parent[x] = (parent[x] == x) ? x : find(parent[x]); } void union(int x, int y) { parent[find(x)] = find(y); } } ``` 8. **树状数组(Fenwick Tree)** - 功能:高效单点更新和前缀查询($O(\log n)$) - 应用:动态频率统计、逆序对计数 9. **线段树(Segment Tree)** - 功能:区间查询与更新($O(\log n)$) - 应用:区间最值、区间和 10. **字典树(Trie)** - 功能:高效存储和检索字符串集合 - 应用:前缀匹配、词频统计 --- #### 四、OJ数据结构的注意事项 1. **输入输出效率** - 使用`BufferedReader`替代`Scanner`以减少I/O耗时[^2]。 2. **内存管理** - 避免频繁创建对象(如循环内`new ArrayList<>()`),防止内存超限。 3. **数据规模与复杂度** - 根据题目限制选择合适结构(如$n \leq 10^5$时需$O(n \log n)$算法)。 --- #### 五、示例应用场景 - **动态数组**:`ArrayList`处理不确定长度的输入数据。 - **频率统计**:`HashMap`统计字符出现次数。 - **最值问题**:`PriorityQueue`解决合并K个有序链表。 - **连通性**:并查集处理社交网络关系[^1]。 ---
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值