链表面试题5之链表分割(拓展必看!)

 欧克,这篇文章我们就要上强度了。

目录

第一种办法:双链表法(哑节点辅助)

哑节点和哨兵位头节点区别

内存的去留

第二种:两次遍历法

第三种:原地重排法

第四种:数组辅助法(C++ STL版)

第五种方法:递归解法


 

这道题非常简洁啊,太好了,说明这是一道比较抽象的题目。但是这道题目被标为了较难题,难在哪里?足足跨过了一个中等的界限。

没错,就是没有图!没有测试案例,这一切都是未知的,那么我们就需要自己去考虑所有的边界情况。当然我觉得越是抽象的题就越爽,你的任何天马行空的情况都包含在内。

这道题我将同时用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;
        }
    }
};

好了,这道题就讲到这里

如果你觉得对你有帮助,可以点赞关注加收藏,感谢您的阅读,我们下一篇文章再见。

一步步来,总会学会的,首先要懂思路,才能有东西写。

 

评论 12
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值