排序算法2(归并排序,快速排序)

归并排序的原理和实现

对于归并排序,重点是要理解分治算法和递归处理的思想。如果要排序一个数组,那么我们首先要把数组从中间分成两段,然后对这两段分别进行排序,最后将排序好的两部分进行性别,这样整个数组就是一个有序的了。首先看一个图进行理解。

归并排序使用的是分治算法的思想。分治,就是分而治之,将一个大问题分解成一个小问题来进行解决。分治算法的思想与之前提到的递归很是相似,分治算法就是通过递归来进行实现的,分治是一种解决问题的思想,递归是一种编程技巧。

编写递归代码的技巧是:写递推公式,寻找终止条件,最后将递推公式和终止条件翻译成代码。所以,现在先来看看递推公式的书写。

递推公式:

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;
}
这里的处理有点类似选择排序。我们通过游标 i 把 A[p…r-1] 分成两部分。A[p…i-1] 的元
素都是小于 pivot 的,我们暂且叫它“已处理区间”,A[i…r-1] 是“未处理区间”。我们
每次都从未处理的区间 A[i…r-1] 中取一个元素 A[j],与 pivot 对比,如果小于 pivot,则
将其加入到已处理区间的尾部,也就是 A[i] 的位置。
数组的插入操作还记得吗?在数组某个位置插入元素,需要搬移数据,非常耗时。当时我们
也讲了一种处理技巧,就是交换,在 O(1) 的时间复杂度内完成插入操作。这里我们也借助
这个思想,只需要将 A[i] 与 A[j] 交换,就可以在 O(1) 时间复杂度内将 A[j] 放到下标为 i
的位置。
如图:

归并排序的处理过程是 由下到上 的,先处理子问题,然后再合并。而快排正好相
反,它的处理过程是 由上到下 的,先分区,然后再处理子问题。归并排序虽然是稳定的、时
间复杂度为 O(nlogn) 的排序算法,但是它是非原地排序算法。我们前面讲过,归并之所以
是非原地排序算法,主要原因是合并函数无法在原地执行。快速排序通过设计巧妙的原地分
区函数,可以实现原地排序,解决了归并排序占用太多内存的问题。

/**
     * 数组内元素交换
     * @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)

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值