单链表排序(归并排序法)

我们已经在“ 单链表排序(冒泡排序法)“中探讨过怎样用冒泡排序法进行单链表的排序,但是在各种排序算法中,冒泡排序并非最高效的,对链表这一特定数据结构而言,最好使用归并排序算法。而堆排序、快速排序这些在数组排序时性能非常好的算法,用在只能“顺序访问“的链表中却不尽如人意(由于无法对链表随机访问,快速排序的的效果并不好,堆排序也是无法实现的),但是归并排序却可以,它不仅保持了O(nlogn)的时间复杂度,而且它的空间复杂度也从O(n)降到了o(1),除此之外,归并排序是分治法的实现。
实现过程中,用到了“ 归并排序(递归实现)“、“ 寻找单链表的中间节点“和“ 合并两个有序的链表(非交叉)“中的知识点。
具体实现如下:

#include <iostream>

typedef struct node
{
    int data;
    struct node *next;
} NODE;


NODE *create_end(int arr[], int len)
{
    NODE *head = (NODE *)malloc(sizeof(NODE *));
    head->next = NULL;
    NODE *end = head;


    for (int i = 0; i < len; i++) {
        NODE *p =  (NODE *)malloc(sizeof(NODE *)); // 也可用 new NODE();
        p->data = arr[i];

        end->next = p;
        end = p;
    }
    end->next = NULL;

    return head;
}


// 此方法适用于不带头节点的单链表的打印,对于带头节点的单链表要稍作处理。
void print(NODE *head)
{
    if (head == NULL) return;

    while (head != NULL) {
        printf("%d  ",head->data);
        head = head->next;
    }
}

// 适用于无头单链表(利用快慢指针找链表的中间位置并将链表一分为二)
// 当链表中有偶数个元素时,最中间的两个节点都可以算作中间节点;
// 但通过该方法查找到的是前面那个节点;如{1,2,3,4,5,6},通过该方法查找到的中间节点是3。
// 将链表一分为二时,要返回第二部分的链表首元素(4)。
NODE *searchMid(NODE *head) // 找中点的同时,拆分链表。
{
    NODE *fast = head;
    NODE *slow = head;
    NODE *mid = NULL;
    NODE *mid_aft = NULL;
    while (fast != NULL && fast->next != NULL && fast->next->next != NULL) // 注意点1:此处用的是fast。
    {
        fast = fast->next->next;
        slow = slow->next;
    }
    mid = slow;
    mid_aft = mid->next;
    mid->next = NULL; // 将链表一分为二
    return mid_aft;
}

// 合并两个有序链表
// 为什么使用引用形式的形参?这样可以不用有返回值,也不用在外部新增一个参数来接受返回值了。
void Merge(NODE *&head1, NODE *head2)
{
    NODE *p1 = head1, *p2 = head2;
    NODE *head3 = (NODE *)malloc(sizeof(NODE)); //合并链表,不需要等长的存储空间;只需开辟一个临时节点(作为头节点)
    NODE *p3 = head3;

    while (p1 != NULL && p2 != NULL) // 注意点2:此处用的是p1和p2,而不是p1->next和p2->next。
    {
        if (p1->data <= p2->data)
        {
            p3->next = p1; // 注意点3:此处用的是p3,而不是head3。
            p3 = p1; // p3在合并过程中,始终指向合并链表的当前尾节点;
            p1 = p1->next;
        }
        else
        {
            p3->next = p2;
            p3 = p2;
            p2 = p2->next;
        }
    }

    if (p1 == NULL) // 注意点4:此处用的是p1,而不是p1->next。
        p3->next = p2;
    if (p2 == NULL)
        p3->next = p1;

    head1 = head3->next;// head1重新指向合并后的链表(head3是头节点,不包含链表数据)
    free(head3); // 注意点5
}

// 归并排序
// 为什么使用引用形式的形参?这样可以不用有返回值,也不用在外部新增一个参数来接受返回值了。
void MergeSort(NODE *&head) {
    if (head == NULL || head->next == NULL) return; // 递归终止条件(当链表长度小于等于1时,即可不用再分了)

    NODE *head1, *head2;
    head1 = head;
    head2 = searchMid(head);

    MergeSort(head1);
    MergeSort(head2);
    Merge(head1, head2);

    head = head1;// 最终归并排序的结果
}

int main(int argc, const char * argv[]) {

    int arr[] = {6,2,5,4,3,7,1,8};
    int len = sizeof(arr)/sizeof(int);
    NODE *head = create_end(arr, len);

    // 排序前
    print(head->next);
    printf("\n");

    // 归并排序
    MergeSort(head->next);

    // 排序后
    print(head->next);
    printf("\n");

    return 0;
}

输出如下:
这里写图片描述

知识点小记:
(1)类比于数组或者连续内存的归并排序。
<1> 单链表归并排序的递归的思路是,将整个链表一直二分,二分成一个个单记录,再反过来对它们执行合并有序链表算法,并层层向上,最终完成对整个链表的排序。
<2> 合并有序链表的算法与合并两个有序数组也有一点不同,即后者需要将总长度的辅助空间“复制”进来。但是对于链表,只需要改变他们的指针即可。即在空间复杂度上降低了。这也是在链表排序上选择归并排序的优势之一。
<3> 另外需要提到的一个技巧,即快慢指针法,此方法利用不同遍历速度的两个指针,可以按照一个比例定位整体链表的位置。这个思想方法可以用在很多地方。

(2)内存泄漏问题

NODE *searchMid(NODE *head)
{
    NODE *fast = head;
    NODE *slow = head;
    NODE *mid = NULL; // 1
    NODE *mid_aft = NULL;
    while (fast != NULL && fast->next != NULL && fast->next->next != NULL)
    {
        fast = fast->next->next;
        slow = slow->next;
        mid = slow;
    }
    mid_aft = mid->next; // 2
    mid->next = NULL;
    return mid_aft;
}

分析:
2 中存在内存泄漏;因为若while循环不成立,那么此行代码就相当于 NULL = NULL->next; 会造成内存泄漏。
解决方案:1中的mid 初始化为 NODE *mid = head 即可。

(3)如果在合并两个有序链表的方法(Merge)中,不想开辟新的存储空间,那么可以考虑做如下修改。

void Merge(NODE *&head1, NODE *head2)
{
    NODE *p1 = head1, *p2 = head2;
    NODE *head3 = NULL;
    NODE *p3 = NULL;
    if (p1->data <= p2->data)
    {
        head3 = p3 = p1;
        p1 = p1->next;
    }
    else
    {
        head3 = p3 = p2;
        p2 = p2->next;
    }

    while (p1 != NULL && p2 != NULL)
    {
        if (p1->data <= p2->data)
        {
            p3->next = p1,
            p1 = p1->next,
            p3 = p3->next;
        }
        else
        {
            p3->next = p2,
            p2 = p2->next,
            p3 = p3->next;
        }
    }

    if (p1 == NULL)
        p3->next = p2;
    if (p2 == NULL)
        p3->next = p1;

    head1 = head3;// head1重新指向合并后的链表
}
  • 8
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值