[C语言]从键盘输入一个长度为 n 的数组,用这个数组组成一个链表,并顺序输出链表每个节点的值。

#include <stdio.h>
#include <stdlib.h>

typedef struct Node {
    int data;           //数据域,可以是任何类型的数据
    struct Node* next;  //指针域
} Node;

//用一个指针表示一个链表
Node* create_list() {
    Node* head_Node = (Node*)malloc(sizeof(Node)); 
    //用一个指针去表示一个表头,headNode变成了结构体变量
    //变量使用前,必须被初始化
    head_Node->next = NULL;
    return head_Node;
}

//创建节点,节点就是结构体变量,只是说我们在创建过程中用指针去表示
//和创建链表的区别是:多一个数据域
Node* create_node(int data) {
    Node* new_Node = (Node*)malloc(sizeof(Node));
    //用指针去表示节点
    //初始化
    new_Node->data = data;
    new_Node->next = NULL;
    return new_Node;
}

//打印节点,一般头节点不存放数据,打印从第二个开始,定义一个额外的结构体指针去打印它
void print_list(Node* head_Node) {
    Node* pMove = head_Node->next; //头指针指向第二个节点,从第二个节点开始打印
    while (pMove) {                //当节点不空
        printf("%d ", pMove->data);
        pMove = pMove->next;
    }
}

//插入函数
//头插法,通过头部的方式(插到以头节点表示的链表,插入的节点数据)
//头插法,数据输入是倒过来的!!!
void insert_Node_by_Head(Node* headNode, int da
<think> 根据引用内容,我们需要完成两个主要部分: 1. 从键盘输入自然数序列(以0结束)构建双向链表打印链表。 2. 对构建好的双向链表进行快速排序,打印排序结果。 注意:双向链表每个节点包含前驱指针(pre)、后继指针(next)和存储数据的成员(data)。 快速排序在链表上的实现通常选择头节点作为基准,然后将链表分为两部分(小于基准和大于等于基准),然后递归排序两个子链表,最后合。 步骤: 1. 定义双向链表节点结构。 2. 实现双向链表的构建(输入自然数,0结束)。 3. 实现双向链表的打印。 4. 实现快速排序(包括分区和递归排序)。 5. 注意:在快速排序过程中,我们需要重新连接节点,所以要注意指针的修改。 由于快速排序是递归的,我们需要设计一个分区函数,该函数以链表的头节点和尾节点为参数,将链表分成两部分,返回基准节点的位置(用于后续递归)。 但是,在双向链表中,快速排序的实现相比数组有一些不同。我们可以这样设计: - 分区函数:选择头节点为基准,然后遍历剩余节点,将小于基准的节点移到基准的前面(形成左链表),大于等于基准的节点保留在基准后面(形成右链表)。但是双向链表是双向的,我们可以利用这个特性。 另一种常见的链表快速排序方法是: 1. 选择头节点作为基准。 2. 建立两个临时链表(可以带哨兵节点),一个存放小于基准的节点一个存放大于等于基准的节点。 3. 遍历链表(基准之后的部分),将每个节点分别插入到两个链表中。 4. 递归对两个链表排序。 5. 将两个链表和基准节点连接起来。 但是,由于是双向链表,我们需要在合时正确设置前后指针。 然而,考虑到双向链表的特性,我们也可以使用前后指针法(类似于数组的快排)但是需要双向调整。 这里我们采用第二种方法(使用两个临时链表): - 使用两个哨兵节点(dummy)来分别存放小于基准和大于等于基准的节点。注意,双向链表需要维护前驱和后继,所以我们在插入节点的时候需要同时调整前后指针。 但是,由于双向链表每个节点有两个指针,我们在将节点放入临时链表时,需要断开与原链表的连接,重新建立新链表中的双向连接。 具体步骤: 1. 如果链表为空或只有一个节点,直接返回。 2. 选择头节点为基准(pivot),将其从链表中断开(注意,这里我们选择的是头节点,所以将头节点取出,然后剩下的链表分为两部分)。 3. 初始化两个双向链表(左链表和右链表),分别用两个哨兵节点来简化操作。 4. 遍历剩余链表(头节点之后的部分),将每个节点与基准比较,然后插入到左链表(小于基准)或右链表(大于等于基准)的末尾,保持双向链表的特性。 5. 递归排序左链表和右链表(注意:排序函数应该返回排序后的链表节点)。 6. 将左链表、基准节点、右链表连接起来(注意双向连接)。 7. 返回连接后的头节点。 注意:在递归排序后,我们需要知道左链表的尾节点来连接基准节点,右链表的头节点来连接基准节点。因此,我们的递归函数返回的是排序后链表的头节点,同时我们还需要返回尾节点(或者另外写一个函数来获取尾节点)。另一种方法是,在分区过程中,我们记录两个链表的头和尾,这样在合时就可以直接连接。 为了简化,我们可以让快速排序函数返回排序后链表的头节点和尾节点,但这样函数需要返回两个(可以使用结构体或指针参数)。或者,我们在排序过程中维护好整个链表的尾节点。 这里我们修改一下思路:在分区时,我们同时记录左链表和右链表的头和尾,这样在合时就可以直接连接。 具体步骤(在快速排序函数中): 1. 如果链表为空或者只有一个节点,直接返回(头节点和尾节点相同)。 2. 选择头节点为基准,将其从链表中断开(即基准节点独立出来,前后指针都置空)。 3. 初始化两个双向链表(左链表和右链表)的哨兵节点分别用两个指针指向它们的当前尾节点(初始为哨兵节点)。 4. 遍历剩余链表(原链表去掉基准节点后的部分),对每个节点: - 如果小于基准,将其插入左链表的末尾(更新左链表的尾指针)。 - 否则,插入右链表的末尾(更新右链表的尾指针)。 5. 递归排序左链表(不包括哨兵节点)和右链表(不包括哨兵节点)。递归调用返回排序后的左链表的头节点和尾节点,右链表的头节点和尾节点。 6. 连接: - 如果左链表非空,将左链表的尾节点指向基准节点,基准节点的前驱指向左链表的尾节点;否则,基准节点的前驱指向空(实际上左链表没有节点,基准节点作为第一个节点,其前驱应该为空,但左链表哨兵的下一个节点为空,所以直接连接即可)。 - 同理,如果右链表非空,将基准节点的后继指向右链表的头节点,右链表的头节点的前驱指向基准节点;否则,基准节点的后继为空。 7. 最后,整个链表节点为:如果左链表非空,则整个链表节点为左链表的头节点(即左链表哨兵节点的下一个节点),否则为基准节点。 整个链表节点为:如果右链表非空,则整个链表节点为右链表的尾节点,否则为基准节点。 8. 注意:我们需要跳过哨兵节点,所以实际的头节点是哨兵节点的下一个节点(如果存在)。 但我们不能把哨兵节点混入结果,所以递归函数处理的是没有哨兵节点链表。因此,在分区时,我们使用哨兵节点是为了方便插入,但递归排序时传入的是哨兵节点后面的链表(即实际链表)。 另一种更简洁的方法是不使用哨兵节点,而是使用头指针和尾指针来维护左右链表。但这样在插入第一个节点时需要特殊处理。 这里我们选择使用哨兵节点简化插入操作,但注意在递归排序时,我们传入的是左链表哨兵->next和右链表哨兵->next(可能为空)。递归函数返回的是排序后的链表节点和尾节点(不包含哨兵节点)。 由于C语言函数只能返回一个,我们可以定义一个结构体来返回头和尾: ```c typedef struct Pair { Node* head; Node* tail; } Pair; ``` 其中Node是双向链表节点。 然后快速排序函数可以这样写: ```c Pair quickSort(Node* head, Node* tail) ``` 但是,我们的初始链表是双向链表且我们可能不知道尾节点?所以,在排序前,我们可以先遍历链表得到尾节点。另外,在递归过程中,我们需要知道链表的头和尾,因为双向链表是双向的,我们可以通过头节点找到尾节点,但是这样效率低,所以我们可以维护尾节点。 但是,在分区过程中,我们已经将链表拆分了,所以我们需要记录每个链表的头和尾。 因此,我们设计快速排序函数如下: ```c // 快速排序函数,输入链表的头节点和尾节点,返回排序后链表的头节点和尾节点 Pair quick_sort(Node* head, Node* tail); ``` 注意:在递归过程中,我们需要断开链表,所以需要知道链表的边界(头节点和尾节点)。 步骤: 1. 如果链表为空(head==NULL)或者只有一个节点(head==tail),返回{head, tail}。 2. 选择头节点为基准节点pivot。 3. 将基准节点链表中断开(注意:如果链表只有一个节点,已经在第一步返回了,所以这里至少有两个节点?不一定,因为递归的时候可能只有一个节点)。所以第一步我们只处理空或一个节点。 4. 然后,将链表分为左链表(小于基准)和右链表(大于等于基准)。注意:在断开基准节点后,剩下的链表是从head->next开始(但此时head已经是基准节点,所以我们之前已经断开,剩下的是原链表中除基准节点外的所有节点,但是双向链表需要重新连接,所以我们在分区时遍历整个链表(除基准节点外))。 但是,这里有一个问题:双向链表如何断开基准节点?实际上,我们传入的头节点和尾节点是当前链表的边界,所以基准节点(即头节点)断开后,左链表和右链表就是剩余部分。 具体步骤: 1. 初始化两个哨兵节点leftDummy和rightDummy,以及它们的尾指针leftTail和rightTail(初始指向哨兵)。 2. 遍历剩余链表(从head->next开始,直到tail为止,注意这里剩余链表实际上是原链表去掉头节点(基准节点)后的部分,但双向链表需要重新连接,所以我们需要用一个指针遍历剩余节点每个节点从原链表中断开,放入左链表或右链表)。 但是,由于我们传入了尾节点且我们知道头节点(基准节点)已经被断开,所以剩余链表的头节点是head->next,尾节点是tail。 但是,注意:在双向链表中,头节点的前驱是NULL,尾节点的后继是NULL。 所以,我们用一个指针cur从head->next开始遍历,直到cur==tail->next(即NULL)结束?但是我们的尾节点是已知的,所以我们可以遍历到tail为止(包括tail)。 遍历过程中: current = cur; cur = cur->next; // 保存下一个节点 然后断开current与链表的连接(即将其前后指针暂时断开?其实不需要,因为我们要放入新链表,所以只需要将其从原链表取出即可。 但是,我们也可以不断开,而是直接改变指针指向。实际上,我们只需要将当前节点从原链表取出,然后添加到左链表或右链表的末尾。 取出当前节点:需要将其前驱和后继断开?但因为我们有前后指针,所以我们可以这样: current->pre->next = current->next; // 但是当前节点可能是头节点(即head->next),那么它的前驱是head,但head已经被我们断开,所以实际上当前节点的前驱应该是NULL?不对,因为原链表中head->next的前驱是指向head的,现在head已经断开(head被取出),所以head->next的前驱应该修改吗?其实我们不需要修改,因为我们把head拿出来了,那么head->next的前驱变成野指针了?所以我们必须修改。 为了避免这种复杂性,我们在分区之前,先将基准节点(head)从链表中移除。然后剩余节点形成一个独立双向链表?但是,这个链表的前驱指针在第一个节点处是断开的(原来指向基准节点,现在基准节点被移除,所以第一个节点的前驱需要指向NULL?但我们没有做这个操作,所以剩余链表实际上是从head->next开始,但head->next的前驱仍然指向head(已经被移除),这是错误的。 因此,我们需要在移除基准节点后,将剩余链表的头节点的前驱置为NULL(如果有头节点的话)。同样,尾节点也要注意。 所以,更简单的方法是不移除基准节点,而是遍历整个链表(包括基准节点),然后跳过基准节点。但是这样在分区时,基准节点已经被选出来了,所以遍历时遇到基准节点就跳过。 这增加了复杂度。 因此,我们选择在分区之前,将基准节点独立出来(即断开它与链表的连接),然后对于剩余链表,我们将其头节点的前驱置为NULL(因为原本指向基准节点),尾节点保持不变(因为尾节点可能变化,所以我们传入尾节点,但尾节点可能被分到左或右链表,所以我们不需要在分区前修改尾节点,而是通过遍历整个剩余链表来构建两个新链表)。 具体步骤: a. 基准节点独立:将基准节点的next和pre都置为NULL。 b. 剩余链表:head = head->next,如果head不为空,则head->pre = NULL(因为原来的头节点被移除,新的头节点前驱应该置空)。同时,如果剩余链表为空(即原链表只有一个节点),那么直接返回基准节点(作为排序后的链表)。 但是,我们传入的是头节点和尾节点,所以我们在函数内部处理剩余链表时,需要重新确定剩余链表的头节点(即原头节点的下一个节点)和尾节点(如果原链表只有一个节点,剩余链表为空;否则尾节点不变?但尾节点可能被分到左或右链表,所以不变)。 所以,我们这样处理: 如果头节点和尾节点相等(即只有一个节点),则返回该节点(在第一步已经处理了,所以分区函数中剩余链表至少有一个节点?不,因为基准节点被移除,剩余链表可能为空)。所以我们在分区函数中需要处理剩余链表为空的情况。 因此,在分区函数中,我们首先判断剩余链表是否为空(即head==NULL),则左右链表都为空。 所以,我们修改步骤: - 如果当前链表(除基准节点)为空,那么基准节点就是整个链表,直接返回基准节点(作为头节点和尾节点)。 - 否则,将剩余链表的头节点的前驱置为NULL(因为基准节点被移除,剩余链表的第一个节点的前驱应该置空?但注意,在双向链表中,头节点的前驱本来就是NULL(除非有哨兵节点),所以我们在构建链表时,头节点的前驱就是NULL?不对,在构建时,我们有一个节点(哨兵节点)?不,在我们的问题中,双向链表没有哨兵节点,所以头节点的前驱是NULL。 所以,实际上,当我们移除基准节点(原头节点)后,剩余链表的头节点(原头节点的下一个节点)的前驱应该指向原头节点,现在原头节点被移除,我们需要将剩余链表的头节点的前驱置为NULL(因为现在它成为新的头节点了)。但这一步是必需的。 但是,如果我们使用哨兵节点构建链表,那么就不需要担心这个问题。但用户要求是自然数序列,输入0结束,所以第一个自然数就是头节点(没有哨兵节点)。 所以,在分区函数中,当我们移除头节点(基准节点)后,我们需要将剩余链表的头节点(如果有)的前驱置为NULL。 同样,在合时,我们需要将基准节点的前驱指向左链表的尾节点,后继指向右链表的头节点相应地设置左链表节点和右链表节点的前后指针。 因此,我们按照以下步骤: - 如果当前链表(除基准节点外)非空,则设置剩余链表的头节点的前驱为NULL(因为现在它是剩余链表的头节点)。 - 初始化两个哨兵节点(左链表和右链表)以及它们的尾指针。 - 用一个指针cur遍历剩余链表(从剩余链表的头节点开始,直到遇到NULL为止,因为剩余链表一个完整的双向链表,它的尾节点后继为NULL)。 - 将每个节点从剩余链表中取出(实际上我们不需要显式取出,因为我们在构建新链表,所以只需要断开它与原链表的连接?其实不需要,因为我们直接把它插入到新链表,所以只需要改变指针即可。但是,由于我们使用哨兵节点且我们将节点添加到新链表的末尾,所以我们需要将当前节点从原链表断开?实际上,剩余链表已经被我们拆分成两个链表,所以不需要保持原链表。 所以,我们直接遍历剩余链表: current = cur; cur = cur->next; // 保存下一个节点 然后根据current->data和基准节点的比较,将其插入到左链表或右链表的末尾(注意双向链表的插入:需要设置current的前驱指向新链表的尾节点,新链表节点的后继指向current,然后更新尾指针为current,且current的后继设为NULL(因为插入到末尾)?但是,双向链表中,插入到末尾后,current的后继应该是NULL吗?实际上,我们还没有遍历完,所以不能设为NULL,而是继续遍历下一个节点。所以这里我们只改变前驱和后继的连接,不需要将current的后继断开,因为cur已经保存了下一个节点。 但是,在插入到新链表时,我们实际上是将current节点添加到新链表的末尾,所以: current->pre = leftTail; // 左链表节点成为current的前驱 leftTail->next = current; // 左链表节点的后继指向current leftTail = current; // 更新左链表节点 // 注意:此时current->next仍然指向原链表的下一个节点?这样会干扰后续遍历。所以我们需要在插入前保存next,且插入后,current->next应该置为NULL吗?不行,因为后面还有节点要处理。 所以,正确的方法是:我们不需要在插入时断开current与剩余链表的连接,因为剩余链表我们之后不再使用,而是通过cur指针(保存下一个节点)继续遍历。因此,在插入后,我们不需要修改current->next,因为下一个节点已经保存在cur中。 但是,当我们把current插入到左链表(或右链表)的末尾时,我们实际上改变了current->next(在下一轮遍历前,cur已经指向了下一个节点,所以没关系)。 然而,在插入过程中,我们会修改current->pre和leftTail->next,这不会影响cur指针(因为cur已经指向下一个节点)。 所以,具体遍历代码: ```c Node* cur = head; // 剩余链表的头节点 while (cur != NULL) { Node* next = cur->next; // 保存下一个节点 if (cur->data < pivot->data) { // 插入左链表 cur->pre = leftTail; leftTail->next = cur; leftTail = cur; } else { // 插入右链表 cur->pre = rightTail; rightTail->next = cur; rightTail = cur; } cur = next; } ``` 遍历结束后,我们需要将左链表和右链表的尾节点的next置为NULL,因为它们是各自链表的最后一个节点。 ```c leftTail->next = NULL; rightTail->next = NULL; ``` 然后,我们得到左链表(leftDummy.next)和右链表(rightDummy.next),以及它们的尾节点(leftTail和rightTail)。 注意:左链表和右链表可能为空(如果左链表为空,则leftDummy.next为NULL,leftTail为leftDummy;同理右链表)。 5. 递归排序左链表和右链表: ```c Pair leftSorted = {NULL, NULL}; if (leftDummy.next != NULL) { leftSorted = quick_sort(leftDummy.next, leftTail); } Pair rightSorted = {NULL, NULL}; if (rightDummy.next != NULL) { rightSorted = quick_sort(rightDummy.next, rightTail); } ``` 6. 合: - 将左链表、基准节点、右链表连接起来。 - 如果左链表非空,则将左链表的尾节点(leftSorted.tail)的后继指向基准节点,基准节点的前驱指向leftSorted.tail;否则,基准节点的前驱为NULL。 - 如果右链表非空,则将基准节点的后继指向右链表的头节点(rightSorted.head),右链表节点的前驱指向基准节点;否则,基准节点的后继为NULL。 - 然后,整个链表的头节点:如果左链表非空,则头节点为leftSorted.head,否则为基准节点。 - 整个链表的尾节点:如果右链表非空,则尾节点为rightSorted.tail,否则为基准节点。 7. 返回合后的链表节点和尾节点。 但是,注意:我们在递归排序左链表时,传入的是leftDummy.next和leftTail(即真实的链表节点和尾节点),递归返回的是排序后的头节点和尾节点。 由于我们使用了哨兵节点,所以左链表和右链表在递归排序时不需要哨兵节点(传入的是实际头节点和尾节点)。 但是,在分区函数中,我们使用哨兵节点是为了方便构建左右链表,递归排序时传入的是实际头节点(leftDummy.next)和尾节点(leftTail),所以没有问题。 最后,注意释放哨兵节点?不,我们在分区函数中创建的哨兵节点是临时变量(在栈上),不需要释放,但是要注意,在递归排序时,我们传入的是实际节点(leftDummy.next),所以哨兵节点没有传入递归函数。 因此,整个快速排序函数实现如下: ```c typedef struct Node { int data; struct Node* pre; struct Node* next; } Node; typedef struct Pair { Node* head; Node* tail; } Pair; Pair quick_sort(Node* head, Node* tail) { // 如果链表为空 if (head == NULL) { Pair res = {NULL, NULL}; return res; } // 如果链表只有一个节点 if (head == tail) { Pair res = {head, head}; return res; } // 选择头节点为基准 Node* pivot = head; // 将基准节点链表中断开,得到剩余链表的头节点(可能为空) Node* remainHead = head->next; // 如果剩余链表非空,则将其头节点的前驱置为NULL(因为基准节点被移除) if (remainHead != NULL) { remainHead->pre = NULL; } // 注意:这里我们断开了基准节点,但基准节点可能还有指向原链表的指针?所以我们将基准节点的next置为NULL pivot->next = NULL; pivot->pre = NULL; // 如果剩余链表为空(即原链表只有基准节点一个节点),则直接返回基准节点 if (remainHead == NULL) { Pair res = {pivot, pivot}; return res; } // 创建链表和右链表的哨兵节点(临时节点,不存储数据,用于简化插入) Node leftDummy, rightDummy; leftDummy.next = NULL; leftDummy.pre = NULL; Node* leftTail = &leftDummy; rightDummy.next = NULL; rightDummy.pre = NULL; Node* rightTail = &rightDummy; // 遍历剩余链表 Node* cur = remainHead; while (cur != NULL) { Node* next = cur->next; // 保存下一个节点 // 将当前节点从剩余链表中断开(实际上不需要显式断开,因为我们会重新连接) // 根据当前节点与基准节点比较 if (cur->data < pivot->data) { // 插入左链表末尾 cur->pre = leftTail; leftTail->next = cur; leftTail = cur; } else { // 插入右链表末尾 cur->pre = rightTail; rightTail->next = cur; rightTail = cur; } cur = next; } // 将左链表和右链表的尾节点的next置为NULL leftTail->next = NULL; rightTail->next = NULL; // 递归排序左链表(leftDummy.next到leftTail)和右链表(rightDummy.next到rightTail) Pair leftSorted = quick_sort(leftDummy.next, leftTail); Pair rightSorted = quick_sort(rightDummy.next, rightTail); // 现在,leftSorted.head和leftSorted.tail是排序后左链表的头和尾 // rightSorted.head和rightSorted.tail是排序后右链表的头和尾 // 合 Node* newHead = NULL; Node* newTail = NULL; // 连接左链表和基准节点 if (leftSorted.head != NULL) { newHead = leftSorted.head; leftSorted.tail->next = pivot; pivot->pre = leftSorted.tail; } else { newHead = pivot; } // 连接基准节点和右链表 if (rightSorted.head != NULL) { pivot->next = rightSorted.head; rightSorted.head->pre = pivot; newTail = rightSorted.tail; } else { pivot->next = NULL; newTail = pivot; } // 如果左链表非空,那么整个链表的头节点是左链表的头节点,否则是基准节点 // 如果右链表非空,那么整个链表的尾节点是右链表的尾节点,否则是基准节点 Pair res = {newHead, newTail}; return res; } ``` 但是,上面的代码有一个问题:在递归排序左链表和右链表时,我们传入的尾节点是分区时得到的左链表和右链表的尾节点(leftTail和rightTail),但是这些尾节点在递归过程中可能被改变(因为递归排序会改变链表结构)?不对,我们在分区函数中,左链表和右链表是由我们构建的,且我们记录了尾节点,但是递归排序后,链表的尾节点会改变吗?可能改变,因为排序后节点顺序变了,所以尾节点可能不是原来的尾节点。因此,我们在递归排序时传入尾节点是不正确的,因为排序后尾节点可能会变。 所以,我们不应该传入尾节点,而是只传入头节点,然后在递归函数内部遍历得到尾节点?但是这样效率低。 因此,我们修改快速排序函数:只传入头节点,然后返回头节点和尾节点。这样,在递归排序左链表和右链表时,我们只传入左链表的头节点(leftDummy.next)和右链表的头节点(rightDummy.next),然后递归函数返回排序后链表的头节点和尾节点。 所以,修改快速排序函数的参数:只传入头节点(对于双向链表,我们可以通过头节点找到尾节点,但这样需要遍历,效率低)。或者,我们修改分区函数,不传入尾节点,而是通过头节点遍历得到尾节点?但这样在分区函数中,我们遍历剩余链表时,需要找到尾节点,因为我们传入的链表是剩余链表,我们需要知道它的尾节点,所以我们在分区函数中遍历一次得到尾节点?这增加了开销。 另一种方法是,我们重写一个函数,可以返回链表的头和尾。例如,在分区函数中,我们遍历剩余链表时,最后得到的leftTail和rightTail就是左右链表的尾节点且我们递归排序时,递归函数返回的是排序后链表的头和尾,这样我们就可以在合时使用。 但是,在递归排序左链表时,我们传入的是左链表的头节点(leftDummy.next),然后递归函数返回排序后的头和尾。同样,右链表也一样。 所以,我们修改:快速排序函数只处理头节点返回头和尾。在分区函数中,我们通过遍历剩余链表来得到左右链表,然后递归排序左右链表(传入的是左右链表的头节点),得到排序后的左右链表的头和尾。 因此,我们修改快速排序函数的签名: ```c Pair quick_sort(Node* head); ``` 然后,在函数内部,如果链表为空或只有一个节点,直接返回头和尾(如果只有一个节点,尾节点就是头节点)。 对于多个节点: 1. 选取头节点为基准,断开头节点与剩余链表。 2. 如果剩余链表非空,则遍历剩余链表(此时我们不知道尾节点,所以需要遍历整个链表来构建左右链表,同时记录左右链表的尾节点)。 3. 递归排序左右链表(传入左链表节点和右链表节点),得到排序后的左右链表的头和尾。 4. 合。 但是,在分区时,我们遍历剩余链表的同时,实际上就可以得到剩余链表长度,但不知道尾节点?我们可以在分区时记录尾节点,但剩余链表是连续的,尾节点就是最后一个非空节点。然而,我们将其拆分成两个链表,所以每个链表的尾节点就是我们插入时记录的那个尾节点。 所以,我们不需要知道剩余链表的尾节点,因为我们拆分成了两个链表每个链表我们都记录了尾节点。 因此,快速排序函数实现如下: ```c Pair quick_sort(Node* head) { if (head == NULL) { Pair res = {NULL, NULL}; return res; } if (head->next == NULL) { // 只有一个节点,那么尾节点就是head Pair res = {head, head}; return res; } Node* pivot = head; Node* remainHead = head->next; // 断开基准节点 pivot->next = NULL; pivot->pre = NULL; if (remainHead != NULL) { remainHead->pre = NULL; } // 创建左右链表的哨兵 Node leftDummy, rightDummy; leftDummy.next = NULL; leftDummy.pre = NULL; Node* leftTail = &leftDummy; rightDummy.next = NULL; rightDummy.pre = NULL; Node* rightTail = &rightDummy; Node* cur = remainHead; while (cur != NULL) { Node* next = cur->next; if (cur->data < pivot->data) { // 插入左链表 cur->pre = leftTail; leftTail->next = cur; leftTail = cur; } else { // 插入右链表 cur->pre = rightTail; rightTail->next = cur; rightTail = cur; } cur = next; } // 设置左右链表结束 leftTail->next = NULL; rightTail->next = NULL; // 递归排序左链表和右链表 Pair leftSorted = quick_sort(leftDummy.next); Pair rightSorted = quick_sort(rightDummy.next); // 合 Node* newHead = NULL; Node* newTail = NULL; // 连接左链表和基准节点 if (leftSorted.head != NULL) { newHead = leftSorted.head; leftSorted.tail->next = pivot; pivot->pre = leftSorted.tail; } else { newHead = pivot; } // 连接基准节点和右链表 if (rightSorted.head != NULL) { pivot->next = rightSorted.head; rightSorted.head->pre = pivot; newTail = rightSorted.tail; } else { newTail = pivot; } Pair res = {newHead, newTail}; return res; } ``` 注意:这里我们递归排序时传入的是leftDummy.next(可能为NULL),所以quick_sort函数内部会处理空链表的情况。 在构建链表时,我们按照用户输入构建双向链表,输入0结束。注意:用户输入的是自然数,0示结束。 构建双向链表(带头节点或不带头节点)?用户提供的引用中,有的使用了头节点(哨兵节点),有的没有。这里我们选择不带头节点(即第一个输入的数作为头节点)。 构建双向链表(正向构建,即新节点添加到链表末尾)?还是逆序?用户要求:键盘输入自然数序列,输入0时结束构建。 根据引用[5]:输入若干个整数(以0结束)逆序构建双向链表。但我们也可以正向构建。 用户作业要求:随机输入自然数序列,0结束。没有指定顺序,所以我们可以选择正向或逆序。但通常,输入序列的顺序就是链表节点顺序。 所以,我们采用正向构建:即新节点添加到链表的末尾。 具体构建步骤: 1. 初始化头节点指针为NULL,尾节点指针为NULL。 2. 读取一个整数,如果为0则结束。 3. 创建一个节点,数据为读取的整数,前驱和后继都置为NULL。 4. 如果链表为空,则头节点和尾节点都指向新节点。 5. 否则,将新节点添加到尾节点之后,然后更新尾节点为新节点。 代码: ```c #include <stdio.h> #include <stdlib.h> typedef struct Node { int data; struct Node* pre; struct Node* next; } Node; Node* createList() { Node* head = NULL; Node* tail = NULL; int data; printf("请输入自然数序列,以0结束:\n"); while (1) { scanf("%d", &data); if (data == 0) { break; } Node* newNode = (Node*)malloc(sizeof(Node)); newNode->data = data; newNode->pre = NULL; newNode->next = NULL; if (head == NULL) { head = newNode; tail = newNode; } else { tail->next = newNode; newNode->pre = tail; tail = newNode; } } return head; } void printList(Node* head) { Node* cur = head; while (cur != NULL) { printf("%d ", cur->data); cur = cur->next; } printf("\n"); } void printListReverse(Node* tail) { Node* cur = tail; while (cur != NULL) { printf("%d ", cur->data); cur = cur->pre; } printf("\n"); } // 快速排序函数 typedef struct Pair { Node* head; Node* tail; } Pair; Pair quick_sort(Node* head) { if (head == NULL) { Pair res = {NULL, NULL}; return res; } if (head->next == NULL) { Pair res = {head, head}; return res; } Node* pivot = head; Node* remainHead = head->next; // 断开基准节点 pivot->next = NULL; pivot->pre = NULL; if (remainHead != NULL) { remainHead->pre = NULL; } // 创建左右链表的哨兵 Node leftDummy, rightDummy; leftDummy.next = NULL; leftDummy.pre = NULL; Node* leftTail = &leftDummy; rightDummy.next = NULL; rightDummy.pre = NULL; Node* rightTail = &rightDummy; Node* cur = remainHead; while (cur != NULL) { Node* next = cur->next; // 判 if (cur->data < pivot->data) { // 插入左链表 cur->pre = leftTail; leftTail->next = cur; leftTail = cur; } else { // 插入右链表 cur->pre = rightTail; rightTail->next = cur; rightTail = cur; } cur = next; } // 设置左右链表结束 leftTail->next = NULL; rightTail->next = NULL; // 递归排序左链表和右链表 Pair leftSorted = quick_sort(leftDummy.next); Pair rightSorted = quick_sort(rightDummy.next); // 合 Node* newHead = NULL; Node* newTail = NULL; // 连接左链表和基准节点 if (leftSorted.head != NULL) { newHead = leftSorted.head; leftSorted.tail->next = pivot; pivot->pre = leftSorted.tail; } else { newHead = pivot; } // 连接基准节点和右链表 if (rightSorted.head != NULL) { pivot->next = rightSorted.head; rightSorted.head->pre = pivot; newTail = rightSorted.tail; } else { pivot->next = NULL; newTail = pivot; } Pair res = {newHead, newTail}; return res; } int main() { // 构建双向链表 Node* head = createList(); printf("原始链表: "); printList(head); // 为了进行快速排序,我们需要得到链表的尾节点(用于之后可能的反向打印,但快速排序函数内部不依赖尾节点,因为递归排序时传入头节点即可) // 但是,在快速排序函数中,我们只传入头节点,返回排序后的头节点和尾节点。 // 所以,我们调用quick_sort(head),返回排序后的头节点和尾节点。 // 但是,在排序前,我们不知道链表的尾节点,所以如果我们需要在排序后得到尾节点,可以通过排序函数返回。 Pair sorted = quick_sort(head); printf("排序后的链表: "); printList(sorted.head); // 释放链表内存(略,根据实际需要) return 0; } ``` 但是,上述代码在递归排序时,对于左链表和右链表,我们传入的是leftDummy.next和rightDummy.next,它们可能为空,所以quick_sort内部会处理。 注意:在分区时,我们将基准节点断开,然后对剩余链表进行分区,这没有问题。 测试:输入 3 1 2 5 4 0,看看排序结果。 但是,有一个潜在问题:在递归排序左链表时,左链表可能包含与基准节点相等的节点?我们的分区条件是:小于基准的放入左链表,大于等于的放入右链表。所以,基准节点(3)会将1,2放入左链表,5,4放入右链表。 然后递归排序左链表:1,2 -> 分区:选择1为基准,剩余2,2>=1,放入右链表。左链表为空,所以排序后左链表返回基准节点1,然后连接右链表(2)-> 1->2。然后与基准3连接:左链表1->2,然后2->3,然后3->4->5。 所以结果是1->2->3->4->5。 但注意,在分区时,我们将
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值