array和list排序算法对比(二):归并排序

接着上一篇文章,这里简单讨论数组和链表的归并排序在算法设计上的区别。
归并排序的特点是采用二分的策略,将数组的子数组进行排序,然后将两个有序的子数组合并成一个大的有序数组。如果采用数组结构,二分是非常简单的操作,但二分后的合并空间开销相对较大。如果采用链表结构,合并的空间开销是相对较小的,但二分则需要精心设计。这也造成了两种数据结构在算法设计上会有一定的差别,复杂度也不同。

再次说明:
1. 排序算法的约定:
sort(begin, end)表示对[begin, end)之间的元素排序,包含begin,但不包含end
2. 在链表中,头部head和尾部tail是不存储数据的。所以,链表的begin对应head->next,end对应tail。

1 数组的归并排序

数组的归并排序算法如下:

void merge_sort(int *begin, int *end){
    if(end - begin <= 1)
        return;

    int *mid = begin + (end - begin) / 2;
    //split
    merge_sort(begin, mid);
    merge_sort(mid, end);
    //merge
    merge(begin, mid, end); 
}

//merge函数
void merge(int *begin, int *mid, int *end){
    int len = end - begin;
    int *tmp = new int[len];
    int *cur = tmp;
    int *left = begin;
    int *right = mid;

    //将较小的数放前面,如果相等,则左边的数放前面
    while(left < mid && right < end){
        if(*left <= *right){
            *cur++ = *left++;
        }else{
            *cur++ = *right++;
        }
    }
    //剩余的数
    while(left < mid){
        *cur++ = *left++;
    }
    while(right <end){
        *cur++ = *right++;
    }
    //复制到原数组
    for(int i = 0; i < len; i++){
        begin[i] = tmp[i];
    }
    //删除临时数组
    delete[] tmp;
}

可见,归并排序的第一步就是int *mid = begin + (end - begin) / 2,这一步二分和后面复杂度为 O(n) 的合并操作,是使归并排序复杂度在 O(nlogn) 的两个关键操作。然而,合并操作的空间复杂度同样为 O(n) ,因此数组归并排序的空间复杂度同样为 O(nlogn) 。这是个不小的空间开销(相比之下,快排是 O(logn) ,堆排序是 O(1) ),有可能会限制归并排序的应用。
接下来,我们可以对照数组的归并排序,给出链表的归并排序。由于链表的内存组织结构与数组不同,链表的二分和归并两步操作都需要采用完全不同的形式来完成。

2 链表的归并排序

在链表的归并排序中,最让人头疼的是链表作为非随机访问的数据结构,很难对齐进行二分操作。如果直接寻找链表的中点,虽然复杂度在渐近意义上不变,但开销仍然让人不能满意。
其实,对链表进行归并排序,并不需要首先找出链表的中点,只需要预先给出链表的长度即可。可以想象,如果我们对链表的前半部分进行排序,排序完成后,自然就获得了链表的中点。而为了对链表的前半部分排序,我们可以先对链表的前1/4部分进行排序……以此类推,我们在排序的过程中,不断后移待排序数组的指针,就可以免去直接查找中点的问题。
在这里,我们给出链表排序函数的接口:

Node* merge_sort(Node *begin, int size);//给出第一个元素和链表的size,而不是给出链表尾部
void merge(Node *begin, Node *middle, Node *end);//[begin, middle)和[middle, end)合并

这里merge_sort的第二个参数是链表的长度,而非链表尾部end,这是为了更好地进行递归操作。有几点需要注意:
1. 在设计链表的时候,通常会直接维护链表的size,因此这个参数的获取可以认为是 O(1) 的复杂度;
2. 在递归过程中,size长度可能小于链表的长度,表示的是对从begin元素开始的size个元素进行归并排序。
3. merge_sort的返回值是链表的尾部,即end,这就是前面提到的“在排序结束时给出链表的end节点”。

给出了这些接口,就可以正式给出链表排序的算法了:

Node* merge_sort(Node *begin, int size){
    if(size == 0){
        return begin;
    }else if(size == 1){
        return begin->next;
    }else{
        Node *begin_prev = begin->prev;
        int left_size = size / 2;
        int right_size = size - left_size;
        Node *middle = merge_sort(begin, left_size);
        Node *middle_prev = middle->prev;
        Node *end = merge_sort(middle, right_size);
        //注意:在两次merge_sort之后,begin和middle指向的节点不再是起点和中点(见merge函数),所以要先保存它们前面的节点,用于找回merge_sort之后的起点和中点。
        merge(begin_prev->next, middle_prev->next, end);
        return end;
    }
}

void merge(Node *begin, Node *middle, Node *end){
    Node *cur1 = begin;
    Node *cur2 = middle;
    //循环结束条件:cur1 == cur2 || cur2 == end
    //cur1 == cur2表示左边列表的指针追上右边,即左边链表遍历结束;
    //cur2 == end表示右边指针到达end,即右边列表遍历结束
    while(cur1 != cur2 && cur2 != end){
        if(*cur1 <= *cur2){
            cur1 = cur1->next; //直接后移cur1指针
        }else{
            //备份cur2指针,然后向后移动cur2指针
            Node *tmp = cur2;
            cur2 = cur2->next;
            //在原链表中移除tmp
            tmp->prev->next = tmp->next;
            tmp->next->prev = tmp->prev;
            //将tmp移动到cur1的前面
            cur1->prev->next = tmp;
            tmp->prev = cur1->prev;
            tmp->next = cur1;
            cur1->prev = tmp;
        }
    }
}

以上即为链表归并排序的算法,可以看出,归并排序的二分过程中,并未实际寻找链表的中点,而是在前半部分排序结束后给出链表的中点。这个思路可以避免不必要的开销。
同时,可以发现,由于merge的时间复杂度为 O(n) ,而空间复杂度只有 O(1) ,因此链表归并排序的时间复杂度为 O(nlogn) ,空间复杂度为O( logn) ,比数组排序要小。因此,对于链表更加适合用归并排序。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
在PHP中,常用的数据结构和算法如下: 数据结构: 1. 数组(Array):一种有序的数据集合,可以通过索引或关联键访问元素。 2. 链表(Linked List):由节点组成的数据结构,每个节点存储数据和指向下一个节点的指针。 3. 栈(Stack):一种后进先出(LIFO)的数据结构,只允许在栈顶进行插入和删除操作。 4. 队列(Queue):一种先进先出(FIFO)的数据结构,允许在队尾进行插入操作,在队头进行删除操作。 5. 哈希表(Hash Table):根据关键字直接访问内存中存储的值,通过哈希函数将关键字映射到数组索引。 算法: 1. 排序算法:如冒泡排序、选择排序、插入排序、快速排序、归并排序等。 2. 搜索算法:如线性搜索、分搜索等。 3. 图算法:如深度优先搜索(DFS)、广度优先搜索(BFS)、最短路径算法(Dijkstra算法、Floyd-Warshall算法)、最小生成树算法(Prim算法、Kruskal算法)等。 4. 动态规划(Dynamic Programming):通过将问题分解为子问题,并保存子问题的解来解决复杂问题。 5. 贪心算法(Greedy Algorithm):每一步选择当前状态下最优的解,以期望达到全局最优解。 6. 回溯算法(Backtracking):通过尝试所有可能的解,并逐步构建可行解的方式来求解问题。 这些数据结构和算法在PHP开发中被广泛应用,用于解决各种问题并提高程序的效率和性能。你可以使用PHP内置的数据结构和算法实现,或者使用第三方库和组件来简化开发过程。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值