归并排序的原理和实现
对于归并排序,重点是要理解分治算法和递归处理的思想。如果要排序一个数组,那么我们首先要把数组从中间分成两段,然后对这两段分别进行排序,最后将排序好的两部分进行性别,这样整个数组就是一个有序的了。首先看一个图进行理解。
归并排序使用的是分治算法的思想。分治,就是分而治之,将一个大问题分解成一个小问题来进行解决。分治算法的思想与之前提到的递归很是相似,分治算法就是通过递归来进行实现的,分治是一种解决问题的思想,递归是一种编程技巧。
编写递归代码的技巧是:写递推公式,寻找终止条件,最后将递推公式和终止条件翻译成代码。所以,现在先来看看递推公式的书写。
递推公式:
merge_sort(p,r) = merge(merge_sort(p,q),merg,merge_sort(q+1,r))
终止条件:p>= r,不在继续分解。
merge_sort(p,r)表示对下标从p到r的数组数据进行递归排序。我们把这个分解成了两个子问题一个是merge_sort(p,q),一个是merge_sort(q+1,r)。其中q,是p,r数组中的中间元素下标,当这两个子问题都排序好了,然后再进行合并也就是merge(),最后得到的数据就是已经排序好的数据。
现在先来看看伪代码
//归并排序算法,A是数组,n是数组大小
merge_sort(A,n){
meoge_sort_c(A,0,n-1);
}
//递归调用函数
merge_sort_c(A,p,r){
//递归终止条件
if(p >= r) then return
//求取中间值
q = (p+r)/2
//分治递归
merge_sort_c(A,p,q);
merge_sort_(A,q+1,r);
merge(A[p,r],A[p,q],A[q+1,r]);
}
分解大家应该都明白了,所以现在的问题就是如何去合并了。接下来我们一点一点分析。
先来看一张图。
从图中我们可以看到,我们需要申请一个临时数组tmp,数组的大小和A[p,r]相同。两个游标i,j分别指向A[p,q]和A[q+1,r]的第一个元素。比较这两个元素,如果A[i]<=A[j]就把A[i]放到临时数组,然后i++,否则A[j]放到临时数组,j++。继续比较,直到其中一个子数组中的所有元素全部放到临时数组tmp中,然后把另一个子数组剩余的元素依次放到临时数组末尾.此时,临时数组存储的就是合并好的数据,并且是有序的,然后再把临时数组tmp复制到数组A中。合并结束。
先来看看伪代码的实现
merge(A[p,r],A[p,q],A[q+1,r]){
var i := p.i := q+1,k := 0;//初始化变量i,j,k
var tmp := new Arr[0,r-p];//初始化一个和A相同大小的数组
//两个子数组进行比较
while i < q And j < r do {
if(A[i] <= A[j]){
tmp[k++] = A[i++]
} else {
tmp[k++] = A[j++];
}
}
//判断哪个子数组还有元素
var start := i,end := q;
if j <= r then start := j,end := r
//将剩余元素复制到临时数组
while start <= end do{
rmp[k++] = A[start++];
}
// 复制回原数组
for i := 0 to r-p do{
A[p+i] == tmp[i];
}
}
下面来看看代码
class Test{
public void mergeSort(int[] a,int start,int end){
if(start < end){
int mid = (start + end)/2;
mergeSort(a,start,mid);//左侧序列进行排序
mergeSort(a,mid + 1,end);//右侧序列进行排序
merge(a,start,mid,end);//合并
}
}
public void merge(int[] a,int left,int mid,int right){
//设置临时数组
int[] temp = new int[a.length];
//设置指针
int p1 = left;
int p2 = mid + 1;
int k = left;//这个k指向temp数组的第一个位置
while(p1 < mid && p2 < right){
if(a[p1] < a[p2]){
temp[k++] = a[p1++];
} else {
temp[k++] = a[p2++];
}
}
//如果左边的序列还有元素没有处理完,直接加到末尾
while(p1 < mid) temp[k++] = a[p1++];
while(p2 < right) temp[k++] = a[p2++];
//将temp的元素复制回a
for(int i : temp){
a[i] = temp[i];
}
}
@Test
public void test(){
int[] a = {2,3,4,5,6,2,4,5,7};
mergeSort(a, 0, a.length-1);
System.out.println("排好序的数组:");
for (int e : a)
System.out.print(e+" ");
}
}
}
归并排序的性能分析
归并排序是稳定性算法
在合并的过程中,如果前半部分A[p,q]和后半部分A{q+1,r]之间有值相同的元素,我们可以先把前半部分A[p,q]中的值相同的元素放入临时数组,再把后半部分值相同的元素放入临时数组,这样就能保证值相同元素在合并前后元素的顺序不变,所以是稳定性排序。
归并排序时间复杂度O(nlogn)
关于归并排序的时间复杂度的分析设计到递归,比较难,所以大家只需要记住时间复杂度为O(nlogn)就可以。
归并排序的空间复杂度O(n)
归并排序在任何情况下的时间复杂度都为O(nlogn),没有被广泛运用的原因是归并排序不是原地排序算法,在进行合并的时候,涉及到了需要借助额外的存储空间tmp。
快速排序的原理和实现
快速排序也用到了分治算法思想,下面来看看原理。
如果要排序数组中下标从p到r的数据。那么,我们选择p到r之间的任意一个数据作为pivot(分区点),然后遍历从p到r的数据,将小于pivot的数据放到左边,将大于或等于pivot的数据放到右边,将pivot放到中间。
经过处理,从p到r的数据就被分成3个部分。假设pivot目前所在的地方为q,那么p到q - 1的部分都小于pivot,q + 1到r的部分都大于pivot。如图。
根据分治的处理思想,分区完成之后,我们递归地排序下标从p到q - 1地数据和下标从q+1到r地数据,直到待排序区间大小缩小为1,这说明数组中所有地数据都有序了,下面用递推公式来表示。
quick_sort(p,r) = partition(p,r) + quick_sort(p,q-1) + quick_sort(q+1,r).
终止条件:p>=r
下面用伪代码来编写
quick_sort(A,n){
//A代表数组,n代表大小
quick_sort_c(A,0,n-1)
}
//快速排序递归函数,p,r为下标
quick_sort_c(A,p,r){
if p >= r then return
q = partition(A,p,r)//分区
quick_sort(A,q+1,r)
}
partition()所做地工作就是随机选择一个元素作为pivot(一般选择p到r区间中地最后一个元素),然后基于pivot对A[p,r]分区。分区函数返回分区之后pivot地下标。
下面来看看快排实现原地分区函数
partition(A,p,r){
pivot := A[r]
i := p
for j := p to r do{
if A[j] < pivot{
swap A[i] with A[j]
i := i + 1
}
}
swap A[i] with A[j]
return i;
}
/**
* 数组内元素交换
* @param nums 输入数组
* @param i 元素1下标
* @param j 元素2下标
*/
private void swap(int[] nums, int i, int j) {
int temp = nums [i];
nums [i] = nums [j];
nums [j] = temp;
}
/**
* 快速排序
*
* @param nums 输入数组
* @param left 划分左边界
* @param right 划分右边界
*/
private void quickSort(int[] nums, int left, int right) {
// 递归返回条件,和分区排序结束
if (right-left <=0) {
return;
}
// 选择数组右边界值作为分区节点
int pivot = nums[right];
// 从数组左边界值开始维护分区
int partition=left-1;
// 遍历当前分区内元素
for (int i = left; i <= right-1; i++) {
if ((nums [i] < pivot) ) {
// 将小于pivot的值交换到partition左边
// 将大于等于pivot的值交换到partition右边
partition++;
swap(nums, partition, i);
}
}
// 将分区节点插入到数组左右分区中间
partition++;
swap(nums, partition, right);
// 分区节点排序完成
// 左分区继续排序,右分区继续排序
quickSort(nums,left, partition-1);
quickSort(nums,partition+1, right);
}
/**
* 排序数组入口函数
*
* @param nums 输入数组
* @return 返回完成排序的数组
*/
public int[] sortArray(int[] nums) {
if (nums == null || nums.length ==0) {
return nums;
}
quickSort(nums, 0, nums.length - 1);
return nums;
}
快速排序不是稳定性排序算法
时间复杂度O(nlogn)
空间复杂度O(n)