赞
踩
归并排序:如果要排序一个数组,我们先把数组从中间分成前后倆个部分,对前后2部分分别排序,再将排好序的2个部分合并在一起,这样整个数组就变成有序的了。
我们通过上面的图解过程可以看出,我们先将一个大的数组分解成一个一个的子问题,再把子问题进行合并,得到最后的结果。这其实就是分治思想。 这其实和我们之前说的递归很相似。其实归并排序使用的就是分治思想,通过递归来实现。基于上面的图解我们试着写出归并排序的代码实现。
通过之前我们知道,写出递归代码的2个主要要素
一:找到递推表达式
二:找到终止条件
其实通过上面的图解过程我们很容易获得递推表达式:
递推公式:mergeSort(p,q) = 合并(mergeSort(p,s),mergeSort(s+1,q));
其中mergeSort(p,q) 表示对数组p,q的下标部分进行归并排序,而下标s我们可以使用p与q的中间位置,即:s = (p+q)/2;得到mergeSort(p,s)与mergeSort(s+1,q)后,我们需要将mergeSort(p,s)与mergeSort(s+1,q)进行合并,合并即对2个有序的子数组合并成一个有序的数组。通过这个我们很容易知道终止条件是p>=q。
有了递归的2个主要要素,我们很容易得到归并排序的代码实现:
// a标识排序的数组 p下标的开始位置 q下标的结束位置 public static int[] mergeSort(int[] a , int p , int q){ int[] result = new int[q-p+1]; // 只有一个元素 if (p == q){ result[0] = a[p]; return result; } int s = (p+q)/2; int[] left = mergeSort(a,p,s); int[] right = mergeSort(a,s+1,q); // 合并2个有序数组 merge(result,left,right); return result; } // 合并2个有序数组 public static void merge(int[] result , int[] left , int [] right){ int leftSize = left.length; int rightSize = right.length; int leftIndex = 0; int rightIndex = 0; int resultIndex = 0; while(leftIndex < leftSize || rightIndex < rightSize){ if (leftIndex >= leftSize){ result[resultIndex] = right[rightIndex]; rightIndex++; } else if (rightIndex >= rightSize){ result[resultIndex] = left[leftIndex]; leftIndex++; }else{ if (right[rightIndex] > left[leftIndex] ){ result[resultIndex] = left[leftIndex]; leftIndex++; }else{ result[resultIndex] = right[rightIndex]; rightIndex++; } } resultIndex++; } }
现在我们来看看排序算法的三个要素:
首先我们很容易知道归并排序是稳定的,因为用到额外的空间存储临时数组,故空间复杂度是O(n);
接下来我们看看时间复杂度: 对于n长度的数组进行归并排序我们容易得到 T(n) = 2*T(n/2) + n,其中n为合并2个有序数组。
如此递归我们可得到 T(n) = 2^k *T(n/2^k) + k * n 当n/2^k = T(1) = 1;可以得到k = logn 从而时间复杂度O(nlogn)。
快速排序我们一般称为快排,在排序算法用的相对比较多,现在让我们看看如何实现快排。
快速排序核心思想:对一个数组进行排序,选择数组中任意一个元素作为分区点pivot,接着遍历数组将大于分区点pivot的元素放在右侧,将小于分区点放到左侧。将分区点pivot放在中间,此时分成3个部分,小于分区点,分区点,大于分区点。
从上述的图解过程我们可以看出,归并是从下到上的过程,而快速排序是一个从上到下的过程。
在快速排序中,对于每次的快排,我们可以将小于分区点的数据放入一个数组,将大于分区点的数据放入另一个数组中,最后合并到数组中.其过程如下:
通过上面的过程,我们可以虽然可以实现快速排序的过程,但是明显使用了额外的内存空间。
那么快速排序便不是原地排序了,如何实现快速排序的原地排序实现呢?
其实我们可以借助选择排序的思想,将数组分成[p,r-1]与[r,q]2个部分,其中[p,r-1]的部分为处理的部分,[r,q]为未处理部分,我们每次取[r,q]中的一个元素与分区点pivot进行比较,如果小于分区点,则加入[p,r-1]的末尾。但是我们知道数组中插入一个元素是极其耗时,那么如何解决这个问题呢?
通过图解我们可以使用指针i与j,从数组的起始位置,如果j的值大于分区点的值, j 指针加一, i 位置不变,直到 j 的值小于等于分区点的值,交换 i 与 j 的值,并且 i 与 j 同事加一, 一直到遍历完数组。
通过上述过程,利用递归,很容易实现快速排序的原地排序的代码:
/* * result:待排序数组 p 起始位置 q 终止位置 */ public static void quickSort(int[] result , int p , int q ){ // 选取最后一个为分区点 int pivot = result[q]; int i = p ; int j = p; while( j < q ){ if(result[j] < pivot){ int temp = result[i]; result[i] = result[j]; result[j] = temp; i++; } j++; } // 交换 i 与 q 的值 int temp = result[i]; result[i] = pivot; result[q] = temp; // 小于分区点 递归执行 直到 长度为1 if (i-1 > p){ quickSort(result,p,i-1); } // 大于分区点 递归执行 直到 长度为1 if (i+1 < q){ quickSort(result,i+1,q); } }
我们来看看快排的性能问题,快排是一个原地排序算法,并且快排不是稳定的,快速也是将一个数组分成近似相同的2个分组,故它的时间复杂度与归并排序相同为O(nlongn)
接下来我们对我们学习的排序算法进行一个简单的总结
排序算法 | 时间复杂度 | 空间复杂度 | 稳定性 |
---|---|---|---|
冒泡排序 | O(n^2) | O(1) | 稳定 |
选择排序 | O(n^2) | O(1) | 稳定 |
选择排序 | O(n^2) | O(1) | 不稳定 |
归并排序 | O(nlongn) | O(n) | 稳定 |
快速排序 | O(nlongn) | O(1) | 不稳定 |
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。