欧克,这篇文章我们就要上强度了。
目录
这道题非常简洁啊,太好了,说明这是一道比较抽象的题目。但是这道题目被标为了较难题,难在哪里?足足跨过了一个中等的界限。
没错,就是没有图!没有测试案例,这一切都是未知的,那么我们就需要自己去考虑所有的边界情况。当然我觉得越是抽象的题就越爽,你的任何天马行空的情况都包含在内。
这道题我将同时用c++和c两种语言,在解决这道题的同时,让大家切实感受到,为什么在c++面向对象并且兼容c的情况下,c语言并没有被兼并的原因。
第一种办法:双链表法(哑节点辅助)
思路:创建两个哑节点作为小链表和大链表的头,遍历原链表,将节点分别添加到对应链表的尾部,最后合并。时间和空间复杂度都是O(n),但空间复杂度是O(1)(只使用几个指针)。
首先讲一下什么是哑巴节点,和我们常说的哨兵位头节点又有什么区别
哑节点和哨兵位头节点区别
哑节点 通常用于简化链表操作中的边界条件处理。它是一个虚拟的节点,本身并不存储实际的数据,而是作为辅助工具帮助开发者更方便地管理链表的操作。
哨兵位头节点 同样是一种特殊的节点,它的主要作用是替代可能存在的空值情况,从而使得链表的操作更加一致化。哨兵节点的存在可以减少对特殊场景(如首节点插入或删除)的单独处理逻辑
都可以视为一种为了优化算法而引入的辅助机制。
无论是哑节点还是哨兵位头节点,都体现了软件工程中的一种重要思想——
通过引入中间层或者过渡状态让整体架构变得更加清晰易懂。
不仅限于单向链表,在双向甚至循环链表里面也非常容易用到,当面对需要频繁修改头部元素的情况时,无论采用哪种形式的前置节点都能极大地提升效率;而在追求极致性能且资源受限的情况下,则需思考是否值得牺牲少量空间换取代码简洁度上的收益。
好了先说这么多,如果你看到这里已经略微有点了感觉,那就可以往下看了,以后再来复习会有更多的体会!
我们直接上代码
// 定义链表节点结构体
struct ListNode {
int val; // 存储节点的值
struct ListNode* next; // 指向下一个节点的指针
};
// 函数声明:按照指定值 x 对链表进行分区
struct ListNode* partition(struct ListNode* head, int x) {
// 创建两个哑节点分别用于存储小于 x 和大于等于 x 的节点
struct ListNode dummy1 = {0, NULL}; // 哑节点1,用于存储小于x的部分
struct ListNode dummy2 = {0, NULL}; // 哑节点2,用于存储大于等于x的部分
struct ListNode* tail1 = &dummy1; // 初始化tail1指向dummy1
struct ListNode* tail2 = &dummy2; // 初始化tail2指向dummy2
struct ListNode* curr = head; // 当前节点初始化为head
// 遍历原始链表
while (curr != NULL) { // 如果当前节点不为空则进入循环
if (curr->val < x) { // 判断当前节点值是否小于x
tail1->next = curr; // 将当前节点链接到tail1之后
tail1 = tail1->next; // 移动tail1至新加入的节点位置
} else { // 若当前节点值大于等于x
tail2->next = curr; // 将当前节点链接到tail2之后
tail2 = tail2->next; // 移动tail2至新加入的节点位置
}
curr = curr->next; // 更新当前节点为下一节点
}
// 处理尾部节点以防形成环状链表
tail1->next = dummy2.next; // 将小于x部分的尾部连接到大于等于x部分的头部
tail2->next = NULL; // 设置大于等于x部分的尾部为NULL
// 返回新的链表头节点
return dummy1.next; // 返回重新组合后的链表头节点
}
通过注释我们很清楚就能看到,哑节点在这里就作为了链表的头节点,把小于x的值链接到smalldummy1后,再把两个链表尾头相接,就完成了。
内存的去留
这里有一个问题,哑巴节点也会占据一定内存,那么我们要像释放哨兵位头节点一样释放他吗?
并不需要,在力扣或者牛客里面,这种做算法题的场景下,很多时候并不要求这一点,只要求我们逻辑结果正确就好了。
在C语言中,动态分配的对象均需显式释放其所占用的内存资源。如果链表中的哑节点是由malloc()或其他类似函数分配而来,则必须在其生命周期结束前调用free()进行释放。这是因为堆上的内存不会随着作用域退出而自动回收;只有栈上的局部变量才会因函数返回而被清空。
class Solution {
public:
ListNode* partition(ListNode* head, int x) {
ListNode small_dummy(0);
ListNode large_dummy(0);
ListNode *small_tail = &small_dummy;
ListNode *large_tail = &large_dummy;
while (head != nullptr) {
ListNode *next = head->next;
head->next = nullptr;
if (head->val < x) {
small_tail->next = head;
small_tail = small_tail->next;
} else {
large_tail->next = head;
large_tail = large_tail->next;
}
head = next;
}
small_tail->next = large_dummy.next;
return small_dummy.next;
}
};
这是c++的代码,涉及到类的知识。我们先看下面方法。
第二种:两次遍历法
思路:第一次遍历收集所有小于x的节点,第二次收集大于等于x的节点,然后连接。需要额外空间存储节点地址,空间复杂度O(n),但实现简单。
struct ListNode* partition(struct ListNode* head, int x) {
// 第一次遍历收集小于x的节点
struct ListNode *small_head = NULL, *small_tail = NULL;
struct ListNode *curr = head;
while (curr != NULL) {
if (curr->val < x) {
struct ListNode *node = (struct ListNode*)malloc(sizeof(struct ListNode));
node->val = curr->val;
node->next = NULL;
if (small_head == NULL) {
small_head = small_tail = node;
} else {
small_tail->next = node;
small_tail = node;
}
}
curr = curr->next;
}
// 第二次遍历收集大于等于x的节点
struct ListNode *large_head = NULL, *large_tail = NULL;
curr = head;
while (curr != NULL) {
if (curr->val >= x) {
struct ListNode *node = (struct ListNode*)malloc(sizeof(struct ListNode));
node->val = curr->val;
node->next = NULL;
if (large_head == NULL) {
large_head = large_tail = node;
} else {
large_tail->next = node;
large_tail = node;
}
}
curr = curr->next;
}
// 连接两个链表
if (small_head == NULL) return large_head;
small_tail->next = large_head;
return small_head;
}
第三种:原地重排法
在遍历过程中,将小于x的节点移动到前面,同时保持顺序。这种方法可能比较复杂,需要仔细调整指针,但空间复杂度O(1)。
struct ListNode* partition(struct ListNode* head, int x) {
struct ListNode *prev = NULL, *curr = head;
struct ListNode *insert_pos = NULL;
// 定位第一个大于等于x的节点
while (curr && curr->val < x) {
insert_pos = curr;
curr = curr->next;
}
while (curr) {
if (curr->val < x) {
// 将curr节点移动到insert_pos后面
struct ListNode *move_node = curr;
curr = curr->next;
prev->next = curr;
if (!insert_pos) { // 需要插入到头部
move_node->next = head;
head = insert_pos = move_node;
} else {
move_node->next = insert_pos->next;
insert_pos->next = move_node;
insert_pos = move_node;
}
} else {
prev = curr;
curr = curr->next;
}
}
return head;
}
除此之外
还有队列法
数组法
递归方法
把代码放在这里,任由读者自行探索
第四种:数组辅助法(C++ STL版)
#include <vector>
class Solution {
public:
ListNode* partition(ListNode* head, int x) {
std::vector<ListNode*> less, ge;
while (head) {
(head->val < x ? less : ge).push_back(head);
head = head->next;
}
ListNode dummy(0);
ListNode* curr = &dummy;
for (auto node : less) {
curr->next = node;
curr = curr->next;
}
for (auto node : ge) {
curr->next = node;
curr = curr->next;
}
curr->next = nullptr;
return dummy.next;
}
};
第五种方法:递归解法
class Solution {
public:
ListNode* partition(ListNode* head, int x) {
if (!head || !head->next) return head;
ListNode* next_part = partition(head->next, x);
head->next = nullptr;
if (head->val < x) {
head->next = next_part;
return head;
} else {
ListNode* curr = next_part;
while (curr->next && curr->next->val < x)
curr = curr->next;
curr->next = head;
return next_part;
}
}
};
好了,这道题就讲到这里
如果你觉得对你有帮助,可以点赞关注加收藏,感谢您的阅读,我们下一篇文章再见。
一步步来,总会学会的,首先要懂思路,才能有东西写。