fucking-algorithm/算法思维系列/学习数据结构和算法的高效方法.md at master · zichangzhang01/fucking-algorithm · GitHub
这篇文章可以全篇背诵
别人的学习笔记也总结的非常好,最好自己总结这样才有思考
面试和 别人一起交路
有人愿意交流的话最好
自顶而上 这是非常重要的信息 符合做任何事情的逻辑
一篇文章 可以不停的补充 直到自己五安全记住
/*基本的单链表节点*/ class ListNode{ int val; ListNode next; } void traverse(ListNode head){ for(ListNode p = head; p != null; p = p.next){ //迭代遍历p.val } } void traverse(ListNode head){ //前序遍历head.val traverse(head.next); //后序遍历head.val } 解释一下链表为啥这样遍历呢 举一个形象生动的例子
在解释为什么链表这样遍历以及提供一个生动的例子之前,我们先纠正一下代码中的一些小问题,并确保我们讨论的是正确的遍历方法。首先,Java中null
应该大写为null
,并且第二个traverse
方法的递归实现方式并不正确,因为它会在尝试访问head.next
之前不检查head
是否为null
,这会导致NullPointerException
。
链表遍历的两种方式
-
迭代遍历
迭代遍历是通过一个循环来逐个访问链表中的每个节点。从链表的头节点开始,一直遍历到链表的末尾(即
next
指向null
的节点)。生动的例子:
想象你手里拿着一串糖葫芦(链表),糖葫芦上串着很多山楂(节点)。你从头开始,一颗一颗地吃(访问)下去,直到吃完最后一颗(遍历到链表末尾)。java复制代码
void traverseIteratively(ListNode head) {
for (ListNode p = head; p != null; p = p.next) {
// 访问p.val,例如打印出来
System.out.println(p.val);
}
}
-
递归遍历
递归遍历是通过函数自身调用自身来遍历链表。对于链表,递归遍历通常是从头节点开始,先处理当前节点(例如访问或打印),然后递归地处理下一个节点。递归遍历需要正确地设置递归终止条件,以避免无限递归,通常这个条件是当前节点为
null
。生动的例子:
想象你进入了一个有很多房间的迷宫(链表),每个房间都有一扇门通向下一个房间(节点间的next
链接)。你从第一个房间开始,先探索当前房间(访问当前节点),然后打开门进入下一个房间(递归调用),继续这个过程,直到你到达一个没有门的房间(链表末尾,next
为null
),这时你知道你已经遍历完了整个迷宫。java复制代码
void traverseRecursively(ListNode head) {
if (head != null) {
// 前序遍历head.val
System.out.println(head.val);
// 递归遍历下一个节点
traverseRecursively(head.
next
);
// 注意:后序遍历(在递归返回之后进行)在这个例子中没有展示,
// 但你可以在递归调用之后添加代码来实现后序逻辑。
}
}
注意
- 在递归遍历中,通常我们只展示前序遍历,因为它在递归调用之前进行。后序遍历可以在递归调用之后进行,但在这个简单的例子中并没有展示。
- 递归遍历需要更多的栈空间,因为它在递归调用时会在调用栈上保存每个函数的局部变量和返回地址,而迭代遍历则不需要这样的额外空间。
- 在实际应用中,选择哪种遍历方式取决于具体的需求和场景。
void traverseRecursively(ListNode head) { if (head != null) { // 前序遍历head.val System.out.println(head.val); // 递归遍历下一个节点 traverseRecursively(head.next); // 注意:后序遍历(在递归返回之后进行)在这个例子中没有展示, // 但你可以在递归调用之后添加代码来实现后序逻辑。 } } 用后序遍历 写这个例子 并且和前序做比较 举个形象生动的例子
在后序遍历中,我们首先递归地遍历子节点(即当前节点的下一个节点),然后处理当前节点。这与前序遍历相反,前序遍历是先处理当前节点,然后递归遍历子节点。
下面是用后序遍历方式修改的traverseRecursively
函数,并给出了与前序遍历的比较以及一个形象生动的例子。
后序遍历函数
java复制代码
void traverseRecursivelyPostOrder(ListNode head) { | |
if (head != null) { | |
// 递归遍历下一个节点(即子节点) | |
traverseRecursivelyPostOrder(head.next); | |
// 后序遍历head.val(在递归返回之后进行) | |
System.out.println(head.val); | |
} | |
} |
前序遍历与后序遍历的比较
(先输出 再递归 和 先递归 再输出 好像没啥区别啊?为啥呢)
前序遍历(Pre-order Traversal):
- 先处理当前节点(例如,访问、打印)。
- 然后递归地处理左子节点(对于二叉树)或下一个节点(对于链表)。
- 形象生动的例子:你进入一座森林(链表或树),每当你到达一棵树(节点)时,你首先摘下树上的苹果(访问节点),然后走进这棵树旁边的第一棵树(递归调用),重复这个过程。
后序遍历(Post-order Traversal):
- 首先递归地处理左子节点(或下一个节点)。
- 然后处理当前节点。
- 形象生动的例子:你进入一座森林,但这次你决定先走过每一棵树旁边的所有树(递归调用),直到你到达森林的边缘,然后再返回并摘下你经过的每棵树上的苹果(访问节点)。
链表后序遍历的生动例子
想象你有一串很长的珍珠项链(链表),每颗珍珠都是一个节点,你站在项链的一端,想要按照后序遍历的顺序来数这些珍珠。你不能先数你眼前的这颗珍珠,因为后序遍历要求你先数完它后面的所有珍珠。所以,你决定先绕过整串项链,走到另一端,然后开始往回数。每当你回到一颗珍珠时,你就数它(访问节点),然后继续往项链的另一端走,直到你数完所有的珍珠。这就是后序遍历链表的过程。
void traverseRecursively(ListNode head) { if (head != null) { // 前序遍历head.val System.out.println(head.val); // 递归遍历下一个节点 traverseRecursively(head.next); // 注意:后序遍历(在递归返回之后进行)在这个例子中没有展示, // 但你可以在递归调用之后添加代码来实现后序逻辑。 } } void traverseRecursivelyPostOrder(ListNode head) { if (head != null) { // 递归遍历下一个节点(即子节点) traverseRecursivelyPostOrder(head.next); // 后序遍历head.val(在递归返回之后进行) System.out.println(head.val); } } 这两个函数有啥区别呢?本质上好像没啥区别呀 可以讲一讲呢?
实际上,这两个函数在功能上是有本质区别的,尽管它们都在处理单链表,并且都使用了递归的方式。主要区别在于它们访问(或“遍历”)链表节点的顺序。
traverseRecursively
函数(前序遍历)
这个函数首先访问(打印)当前节点head
的值,然后递归地调用自身来处理下一个节点(head.next
)。这意味着它按照链表中的顺序从头到尾访问每个节点,并在访问任何节点之前,先访问其前面的所有节点。然而,在单链表的上下文中,我们通常不称之为“前序遍历”,因为“前序遍历”这个术语更多地与二叉树相关,其中涉及先访问根节点,然后是左子树,最后是右子树。但在单链表中,我们可以简单地将它理解为“先访问”每个节点的遍历方式。
traverseRecursivelyPostOrder
函数(后序遍历)
这个函数首先递归地调用自身来处理下一个节点(head.next
),然后在返回后(即递归调用完成后)访问当前节点head
的值。这意味着它按照链表中的顺序从尾到头访问每个节点,即在访问任何节点之前,先访问其后面的所有节点。这种遍历方式在单链表中通常被称为“后序遍历”,尽管它与二叉树中的后序遍历(先左子树,再右子树,最后根节点)在结构上有所不同,但基本思想是先递归到底,再逐层返回并处理节点。
形象生动的例子
-
前序遍历:想象你正在读一本书,从第一页开始,一页一页地往后读。这就是前序遍历在链表中的表现,你从链表的头节点开始,逐个访问每个节点。
-
后序遍历:现在想象你正在参加一个聚会,你需要和在场的每个人打招呼,但你决定先从最后到达的人开始打招呼,然后再逐个向前,直到和最先到达的人打招呼。这就是后序遍历在链表中的表现,你从链表的尾节点开始(尽管在递归实现中,你实际上是先深入到链表的末尾,然后再逐个返回并访问节点),但逻辑上是从后往前的。
总结
尽管这两个函数在代码结构上看起来相似(都使用了递归),但它们在遍历链表时的顺序上是相反的。前序遍历先访问当前节点,后序遍历则先递归到链表末尾,再逐个返回并访问节点。
chatgpt的回答更好
当然可以讲讲这两个函数的区别。虽然看起来它们在结构上非常相似,但在遍历顺序和用途上有显著区别。
### 前序遍历 (Pre-order Traversal)
```java
void traverseRecursively(ListNode head) {
if (head != null) {
// 前序遍历head.val
System.out.println(head.val);
// 递归遍历下一个节点
traverseRecursively(head.next);
}
}
```
在这个函数中,节点的值 (`head.val`) 是在递归调用之前打印的,这意味着当前节点在其子节点之前被处理。这种遍历顺序称为前序遍历。在前序遍历中,节点的处理顺序是先处理当前节点,然后处理其子节点。
**应用场景**:
- 可以用于创建节点的副本,或在节点尚未被其子节点处理时需要操作它们的场景。
### 后序遍历 (Post-order Traversal)
```java
void traverseRecursivelyPostOrder(ListNode head) {
if (head != null) {
// 递归遍历下一个节点(即子节点)
traverseRecursivelyPostOrder(head.next);
// 后序遍历head.val(在递归返回之后进行)
System.out.println(head.val);
}
}
```
在这个函数中,节点的值 (`head.val`) 是在递归调用之后打印的,这意味着当前节点在其子节点之后被处理。这种遍历顺序称为后序遍历。在后序遍历中,节点的处理顺序是先处理其子节点,然后处理当前节点。
**应用场景**:
- 常用于需要在节点的子节点全部处理完毕之后再处理节点的场景,例如删除文件夹及其内容,计算子节点的总和等。
### 区别总结
1. **处理顺序**:
- **前序遍历**:先处理当前节点,然后处理子节点。
- **后序遍历**:先处理子节点,然后处理当前节点。
2. **用途**:
- **前序遍历**:适用于需要在处理节点的子节点之前操作当前节点的情况。
- **后序遍历**:适用于需要在处理节点的子节点之后操作当前节点的情况。
### 举例
假设有一个链表 `1 -> 2 -> 3 -> null`,它们的输出如下:
- **前序遍历**:
```java
traverseRecursively(head); // Output: 1 2 3
```
- **后序遍历**:
```java
traverseRecursivelyPostOrder(head); // Output: 3 2 1
```
这种区别在树结构中更为明显,但即便在链表中,这种遍历顺序的差异依然存在并且可能影响特定操作的效果。例如,如果你需要反向处理链表中的元素,后序遍历会更适合。