归并排序及深入
归并排序是一种思想,而不是一段实现代码。是将各个子数组排好序,再整体排序的思想。比如大数据处理中的多路归并。这里以典型的二路归并为例
借助于辅助数组,空间复杂度为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;
}