一个认为一切根源都是“自己不够强”的INTJ
个人主页:用哲学编程-CSDN博客
专栏:每日一题——举一反三
Python编程学习
Python内置函数
Python-3.12.0文档解读
目录
我的写法
专业点评
时间复杂度分析
空间复杂度分析
总结
我要更强
方法2:使用哈希表
方法3:修改节点结构(不推荐)
方法4:反转链表(不推荐)
结论
哲学和编程思想
快慢指针 (Floyd's Cycle-Finding Algorithm)
哈希表法
修改节点结构法
反转链表法
举一反三
技巧1:双指针技术
技巧2:哈希表技术
技巧3:状态标记法
技巧4:反转操作
技巧5:递归与迭代
技巧6:分治法
技巧7:贪心算法
技巧8:动态规划
技巧9:回溯法
技巧10:模拟法
题目链接
我的写法
/** * Definition for singly-linked list. * struct ListNode { * int val; * struct ListNode *next; * }; */bool hasCycle(struct ListNode *head) {// 定义两个指针,slow 和 quick,初始都指向链表的头节点struct ListNode *slow = head;struct ListNode *quick = head;// 如果链表为空或只有一个节点,则不可能存在环,返回 falseif (!head || !head->next) {return false;}// 使用 for 循环来遍历链表,循环条件是 slow 和 quick 及其下一个节点都不为 NULLfor (; slow && slow->next && quick && quick->next && quick->next->next;) {// slow 指针每次移动一步slow = slow->next;// quick 指针每次移动两步quick = quick->next->next;// 如果 slow 和 quick 指针相遇,则说明链表中存在环if (slow == quick) {return true;}}// 如果循环结束后没有相遇,说明链表中不存在环,返回 falsereturn false;
}
这段代码实现了一个检测单链表是否存在环的功能,采用的是经典的快慢指针(Tortoise and Hare)算法。下面是对这段代码的专业点评及其时间复杂度和空间复杂度分析。
专业点评
- 代码结构清晰:
- 代码结构合理,使用变量命名清晰,便于理解。
- 初始检查部分有效地处理了特殊情况(空链表或只有一个节点的链表)。
- 循环条件:
- 循环条件健全,确保在链表遍历过程中不会发生空指针引用错误。
- for 循环中使用多个条件确保 slow 和 quick 及其下一个节点都有效。
- 快慢指针逻辑:
- 快指针每次移动两步,慢指针每次移动一步,有效地检测链表中的环。
- 如果存在环,快慢指针必定会相遇。
- 代码简洁性:
- 代码简洁,逻辑明确,没有冗余的操作。
时间复杂度分析
- 最坏情况:在没有环的情况下,快指针和慢指针遍历链表的每个节点。
- 在这种情况下,快指针遍历节点数大约为 n/2,慢指针遍历节点数为 n,其中 n 是链表的节点数。
- 因此,时间复杂度为 O(n)。
- 最优情况:在存在环的情况下,快慢指针会在环内相遇。
- 最多需要 O(n) 的时间,因为快指针和慢指针最终会在环内相遇。
综合来看,时间复杂度为 O(n)。
空间复杂度分析
- 该算法只使用了两个额外的指针变量 slow 和 quick,不依赖于链表的大小。
- 因此,空间复杂度为 O(1)。
总结
- 优点:
- 时间复杂度为 O(n),在所有链表节点遍历完毕前即可得出结果。
- 空间复杂度为 O(1),只用到了常数额外空间。
- 代码简洁清晰,易于理解。
- 缺点:
- 无明显缺点,该算法是检测链表环的经典方法,几乎是最优解。
改进建议
- 从代码功能和效率来看,这段代码已经相当高效且简洁,没有明显需要改进的地方。如果需要进一步优化,可以考虑在注释方面增加更详细的解释,以便于其他开发者更好地理解代码逻辑。
代码
/*** Definition for singly-linked list.* struct ListNode {* int val;* struct ListNode *next;* };*/
bool hasCycle(struct ListNode *head) {// 定义两个指针,slow 和 quick,初始都指向链表的头节点struct ListNode *slow = head;struct ListNode *quick = head;// 如果链表为空或只有一个节点,则不可能存在环,返回 falseif (!head || !head->next) {return false;}// 使用 for 循环来遍历链表,循环条件是 slow 和 quick 及其下一个节点都不为 NULLfor (; slow && slow->next && quick && quick->next && quick->next->next;) {// slow 指针每次移动一步slow = slow->next;// quick 指针每次移动两步quick = quick->next->next;// 如果 slow 和 quick 指针相遇,则说明链表中存在环if (slow == quick) {return true;}}// 如果循环结束后没有相遇,说明链表中不存在环,返回 falsereturn false;
}
这段代码实现了高效的环检测功能,是生产环境中常用的方法之一。
我要更强
在链表检测环的问题中,代码已经使用了快慢指针(Floyd’s Cycle-Finding Algorithm),这已经是时间复杂度 (O(n)) 和空间复杂度 (O(1)) 的最优解。因此,从理论上讲,无法进一步优化这两个指标。
不过,为了完整性和拓展视野,提供几个不同的方法,即使它们在时间和空间复杂度上未必能优于快慢指针法。下面将详细介绍这些方法并提供相应的代码和注释。
方法2:使用哈希表
利用哈希表记录每个访问过的节点,如果再次遇到已经访问过的节点,则说明存在环。时间复杂度为 (O(n)),空间复杂度为 (O(n))。
#include <stdbool.h>
#include <stdlib.h>// Definition for singly-linked list.
struct ListNode {int val;struct ListNode *next;
};// Definition of the hash table node.
struct HashNode {struct ListNode *node;struct HashNode *next;
};// Simple hash function for the hash table.
unsigned int hash(struct ListNode *node) {return ((unsigned long)node) % 1024;
}bool hasCycle(struct ListNode *head) {if (!head) return false;struct HashNode *hashTable[1024] = { NULL };while (head) {unsigned int hashIndex = hash(head);struct HashNode *entry = hashTable[hashIndex];while (entry) {if (entry->node == head) {return true; // Cycle detected}entry = entry->next;}struct HashNode *newEntry = (struct HashNode *)malloc(sizeof(struct HashNode));newEntry->node = head;newEntry->next = hashTable[hashIndex];hashTable[hashIndex] = newEntry;head = head->next;}return false; // No cycle detected
}
方法3:修改节点结构(不推荐)
这种方法临时修改节点结构,标记访问过的节点。虽然时间复杂度和空间复杂度均为 (O(n)),但破坏了链表的原始结构。
// Definition for singly-linked list.
struct ListNode {int val;struct ListNode *next;
};bool hasCycle(struct ListNode *head) {while (head) {// 如果当前节点的值已经被标记,则说明存在环if (head->val == -1) {return true;}// 标记当前节点head->val = -1;head = head->next;}return false; // 如果遍历完成后没有发现环
}
方法4:反转链表(不推荐)
这种方法通过反转链表来检测环,但会破坏链表的结构,且复杂度较高。
// Definition for singly-linked list.
struct ListNode {int val;struct ListNode *next;
};struct ListNode* reverseList(struct ListNode* head) {struct ListNode* prev = NULL;struct ListNode* curr = head;while (curr) {struct ListNode* nextTemp = curr->next;curr->next = prev;prev = curr;curr = nextTemp;}return prev;
}bool hasCycle(struct ListNode* head) {if (!head || !head->next) {return false;}struct ListNode* reversedHead = reverseList(head);// 如果反转后的链表头指针等于原链表头指针,说明有环return reversedHead == head;
}
结论
在检测链表环的问题上,快慢指针算法已经是最优解,具有 (O(n)) 时间复杂度和 (O(1)) 空间复杂度。其他方法虽然也能实现环检测,但在性能和实用性上不如快慢指针高效。
哲学和编程思想
在解决链表环检测问题的过程中,可以看到一些重要的哲学和编程思想。这些思想不但帮助我们理解和解决问题,还可以提升编写高效、可靠代码的能力。
快慢指针 (Floyd's Cycle-Finding Algorithm)
哲学思想:
- 对称性与均衡:通过两个不同步的指针(快指针和慢指针),我们可以同时观察链表的不同部分。这个方法体现了对称性和均衡的哲学思想。
- 渐进性:慢指针逐步遍历链表每一步,而快指针以更快的速度前进。这个思想类似于在哲学中逐步接近真理的过程。
编程思想:
- 双指针技术:利用两个指针以不同速度遍历数据结构。
- 空间效率:仅使用固定数量的额外空间,使算法在空间复杂度上最优。
哈希表法
哲学思想:
- 记忆与记录:通过记录已经访问过的节点,我们可以检测到重复。这个方法类似于人类记忆和识别重复事件的过程。
- 关联性:哈希表的使用体现了关联性哲学,通过快速定位和匹配记录来检测环的存在。
编程思想:
- 哈希表:利用哈希表的快速查找特性来记录和检测节点。
- 时间效率:虽然增加了空间复杂度,但提高了时间效率。
修改节点结构法
哲学思想:
- 变更与保留:通过临时变更节点的值或结构来标记已访问的节点,体现了变更与保留的哲学思想。
- 牺牲与利益:在一定程度上牺牲链表的完整性来换取检测环的简便性。
编程思想:
- 状态标记:通过修改数据结构的状态来记录访问情况。
- 时间效率:虽然这种方法简单直接,但不推荐因为它破坏了原始数据结构。
反转链表法
哲学思想:
- 反转与复原:通过反转链表实现检测环,体现了反转与复原的哲学思想。
- 对立统一:链表的反转与检测过程展现了对立统一的哲学观点。
编程思想:
- 反转操作:利用链表反转的技巧来检测环。
- 结构变化:尽管能实现目标,但会破坏链表结构,因此在实际应用中不推荐。
总结
这些方法展示了不同的哲学和编程思想,包括对称性、记忆与记录、变更与保留、反转与复原等。在解决问题时,理解这些思想不仅帮助选择适当的算法,还可以提高编程能力和对问题的深刻理解。
举一反三
为了帮助你在编程中举一反三,下面提供了一些基于哲学和编程思想的技巧。这些技巧将帮助你在不同情境下应用这些思想:
技巧1:双指针技术
哲学思想:对称性与均衡
应用场景:
- 排序数组中的问题,如两数之和、三数之和。
- 快慢指针用于链表问题,如检测链表环、找到中间节点。
- 滑动窗口技术用于子数组问题,如最大子数组和、最小覆盖子串。
思维方式:
- 考虑从两端同时向中间推进。
- 或者从头和尾同时进行遍历,找到符合条件的值。
技巧2:哈希表技术
哲学思想:记忆与记录
应用场景:
- 查找问题,如查找数组中是否存在重复元素。
- 计数问题,如字符频率统计、两数组交集。
- 映射问题,如从一个集合映射到另一个集合。
思维方式:
- 利用快速查找的特性,记录已经访问过的元素及其信息。
- 适用于需要频繁查找和更新数据的问题。
技巧3:状态标记法
哲学思想:变更与保留
应用场景:
- 动态规划问题,如最小路径和、背包问题。
- 图的遍历问题,如深度优先搜索(DFS)、广度优先搜索(BFS)。
- 状态转移问题,如记忆化搜索、状态压缩。
思维方式:
- 通过记录状态来避免重复计算。
- 使用数组或其他数据结构保存中间结果。
技巧4:反转操作
哲学思想:反转与复原
应用场景:
- 数据结构的反转,如反转链表、反转字符串。
- 双端队列问题,如使用双指针反转数组的一部分。
- 操作顺序颠倒问题,如栈的应用。
思维方式:
- 考虑如何从尾到头进行操作,或从两端向中间靠拢。
- 适用于需要反转或改变顺序的问题。
技巧5:递归与迭代
哲学思想:自相似性
应用场景:
- 分治算法,如快速排序、归并排序。
- 树的遍历,如前序、中序、后序遍历。
- 数学问题,如斐波那契数列、组合数计算。
思维方式:
- 递归方法通常用于自然分治的问题。
- 确保理解递归的基线条件和递归条件。
- 考虑如何将递归转换为迭代来提高效率。
技巧6:分治法
哲学思想:整体与部分
应用场景:
- 排序算法,如快速排序、归并排序。
- 搜索算法,如二分搜索、最近点对问题。
- 动态规划问题中的优化,如矩阵链乘法。
思维方式:
- 将问题分解为更小的子问题,解决每个子问题后合并结果。
- 适用于可以自然分割的问题。
技巧7:贪心算法
哲学思想:局部最优与全局最优
应用场景:
- 路径问题,如最短路径、最小生成树。
- 资源分配问题,如活动选择、背包问题。
- 排序问题,如区间调度、会议室安排。
思维方式:
- 在每一步选择局部最优解,希望通过一系列局部最优解达到全局最优。
- 适用于贪心选择性质和最优子结构的问题。
技巧8:动态规划
哲学思想:阶段性最优
应用场景:
- 序列问题,如最长上升子序列、最长公共子序列。
- 数组问题,如最大子数组和、矩阵路径问题。
- 计数问题,如硬币兑换、字符串匹配。
思维方式:
- 分阶段求解问题,每个阶段的解依赖于上一个阶段的解。
- 使用状态转移方程和初始条件来解决问题。
技巧9:回溯法
哲学思想:试探与回溯
应用场景:
- 组合问题,如全排列、组合数。
- 棋盘问题,如八皇后、数独。
- 路径问题,如迷宫求解、图的 Hamiltonian 路径。
思维方式:
- 通过试探法尝试所有可能的解,当发现当前试探不成功时,进行回溯。
- 适用于需要穷举所有解或找到一个可行解的问题。
技巧10:模拟法
哲学思想:逐步逼近
应用场景:
- 操作模拟,如旋转矩阵、文字处理。
- 数学问题,如大数计算、数列生成。
- 游戏问题,如棋盘游戏、博弈问题。
思维方式:
- 逐步模拟问题的步骤,接近最终结果。
- 考虑如何将实际问题的步骤逐步实现。
通过理解和应用这些技巧,可以在不同类型的编程问题中举一反三,从而提高解决问题的效率和灵活性。