归并排序及深入

归并排序是一种思想,而不是一段实现代码。是将各个子数组排好序,再整体排序的思想。比如大数据处理中的多路归并。这里以典型的二路归并为例

借助于辅助数组,空间复杂度为o(n)的二路归并,见自己之前的博客

http://www.cnblogs.com/bobodeboke/p/3416716.html

以此衍生出来的题目:

// ***衍生出来的题目一:给两个有序数组a和b,已知数组a的末尾还有足够的空间可以容纳b,写一个函数将b合并到数组a中
// 和归并排序中mergeArr的方法没什么区别,使用两个指针,对比这两个指针的当前元素;只不过从最大下标处开始归并

 

// ****归并衍生问题二:原地归并*************************/
// 题目:数组arr[0,mid-1]和数组arr[mid,num-1]都 有序,将其merge为一个有序数组arr[0,num-1]
// 限制条件,要求空间复杂度为O(1)

对于原地归并,可以参见这篇博客http://blog.csdn.net/zhongkeli/article/details/8786694

注意点:

1,重点是手摇发实现内存交换,所谓手摇法。是对要求两个数据序列序列内顺序不变,但两个序列的位置改变的问题。比如之前碰到的字符串左旋

2,注意一前一后两个指针的转化

// 如果没有控件复杂度的要求,那么采用归并排序中类似的归并算法,其空间复杂度为O(n)
    public void merge(int[] arr, int mid) {
        int[] tmpArr = new int[arr.length];
        int i = 0;
        int j = mid;
        int k = 0;
        while (i < mid && j <= arr.length - 1) {
            if (arr[i] <= arr[j]) {
                tmpArr[k++] = arr[i++];
            } else {
                tmpArr[k++] = arr[j++];
            }
        }
        while (i < mid) {
            tmpArr[k++] = arr[i++];
        }
        while (j <= arr.length - 1) {
            tmpArr[k++] = arr[j++];
        }
        // 重新复制到arr数组中
        for (i = 0; i < k; i++) {
            arr[i] = tmpArr[i];
        }
    }

    // 因为要求空间复杂度为O(1),因此采用原地归并
    // 原地归并排序所利用的核心思想便是“反转内存”的变体,即“交换两段相邻内存块”,对于反转内存的相关文章,曾在文章“关于反转字符串(Reverse
    // Words)的思考及三种解法”中对一道面试题做了分析。这一思想用到的地方很多,在《编程珠玑》中被称为“手摇算法”。
    // 也就是两块交换内存,两个块的位置发生变化,而每一个块内部的相对顺序保持不变
    public void merge2(int[] array, int mid) {
        // 使用一前一后两个指针
        int end = array.length - 1;
        // 第一个有序子数组开始的地方
        int i = 0;
        // 第二个有序子数组开始的地方
        int j = mid;
        // 第一步:i向后移动,找到第一个array[i]>array[j]的位置,这时0——i-1一定是数组整体最小块
        while (i < j && j <= end) {
            // 注意外层判断了i<j,并不能代表里层继续成立
            while (i < j && array[i] <= array[j]) {
                i++;
            }
            // 之后记录下index=after,再让after向后走,知道找到第一个array[after]>array[pre]的部分;这个时候mid-after-1的整体一定小于pre-mid
            int old_j = j;
            while (j <= end && array[j] <= array[i]) {
                j++;
            }

            // 接下来pre_mid-1和mid——after交换内存,采用手摇法,两次块内翻转,依次整体翻转
            shouyao(array, i, old_j, j-1);
            // 之后将pre移动[old_after,after)个距离,前面的顺序排列,后面又构成两个有序子数组
            i += j - old_j;

        }
    }

    private void shouyao(int[] array, int pre, int mid, int after) {
        reverse(array, pre, mid - 1);
        reverse(array, mid, after);
        reverse(array, pre, after);
    }

    private void reverse(int[] array, int pre, int end) {
        for (int i = pre, j = end; i <= j; i++, j--) {
            swap(array, i, j);
        }

    }

    private void swap(int[] array, int i, int j) {
        int tmp = array[i];
        array[i] = array[j];
        array[j] = tmp;
    }
手摇法实现原地归并

 

 

/******衍生问题三,归并及堆排序在大数据中的运用*******/

题目1,从n个元素中选择前k小的元素

方法一:快排的思路,见上篇博客。时间复杂度为O(n)

不过这种方法的缺点,一是需要将所有的元素读入内存;而是需要修改输入的数组

方法二:

1)取前面k个元素组成一个最大堆

2)从剩余的元素中依次取出一个元素和堆顶元素进行比较,如果比堆顶元素小,那么替换堆顶元素,并进行一次自上而下的调整

3)以此类推,最终堆中剩余的k个元素即是前k个最小的元素(注意堆排序中adjustDown和adjustUp的实现)

题目2,有20个有序数组,每个数组有500个int元素,降序排列。要求从这10000个元素中选出最大的500个。


1)从20个数组中各取出一个数,并记录每个数的来源数组,建立一个20个元素的大根堆。此时堆顶就是当前最大的元素,取出堆顶元素(不同于上一题,上一题是抛弃堆顶元素)

2)从堆顶元素的来源数组中取出下一个数加入堆,调整堆后,再取最大值

3)如此进行500次即可。时间复杂度为(o(nlog2k))

备注:

1)其实分成20个较小的数组,并且内部先排好序的过程就是归并的过程

2)例题1)取出最小的k个元素;和例题2中取出最大的k个元素都是采用最大堆。其实关键是看抛弃堆顶元素还是保留堆顶元素

3)例题2因为需要记录元素的来源,因此需要封装一种结构

struct Elemtype{

int elem;

int source;

}

posted @ 2014-08-27 17:56  bobo的学习笔记  阅读(249)  评论(0编辑  收藏  举报