漫画算法-学习笔记(14)

漫画算法-小灰的算法之旅(14)

1. 什么是快速排序

和冒泡排序一样,快速排序也属于交换排序,通过元素之间的比较和交换位置来达到排序的目的。

不同的是,冒泡排序在每一轮中只把一个元素冒泡到数列的一端,而快速排序则在每一轮挑选一个基准元素,并让其他比它大的元素移动到数列一边,比它小的元素移动到数列的另一边,从而把数列拆解成两个部分。这种思路就叫分治法

所谓分治法的思想:

  • 从数组中取一个数,称之为基数(pivot)
  • 遍历数组,将比基数大的元素放到它的右边,比基数小的元素放到它的左边。遍历完成后,数组被分成了左右两个区域。
  • 将左右两个区域视为两个数组,重复前两个步骤,直到排序完成。

事实上,快速排序的每一次遍历,都将基数摆到了最终位置上,第一轮遍历排好1个基数,第二轮遍历排好2个基数(每个区域一个基数,但如果某个区域为空,则此轮只能排好一个基数),第三轮遍历排好4个基数(同理最差情况下,只能排好一个基数),依次类推,总共遍历次数为lognn^2次,每一轮遍历的时间复杂度为O(n),因此快排的时间复杂度为O(nlogn)O(n^2),平均时间复杂度为O(nlogn).

基准元素的选择

基准元素(pivot):在分治过程中,以基准元素为中心,把其他元素移动到它的左右两边。

如何选择基准元素呢?

  1. 最简单的方式是选择数组的第一个元素。 但当将一个逆序的数列,排序成顺序数列时,此时采用快速排序算法,会发现整个数列并没有被分成两半,每一轮都只确定了基准元素的位置,此场景下,快速排序的时间复杂度为O(n^2).
  2. **随机选择一个元素作为基准元素。**并且让基准元素和数列首元素交换位置。这样,即使在数组完全逆序的情况下,也能有效降数组分为两部分;以满足分治法的思想。
元素的交换

选定了基准元素以后,我们要做的就是把其他元素中小于基准元素的都交换到基准元素一边,大于基准元素的都交换到基准元素另一边。

  • 双边循环法
  • 单边循环法
双边循环法

什么是双边循环法?

给出原始数列如下,要求对其从小到大进行排序

首先,选定基准元素pivot,并且设置两个指针left和right,指向数列的最左和最右两个元素。

接下来进行第一次循环,从right指针开始,让指针所指向的元素和基准元素做比较,如果大于或等于pivot,则指针向左移动,如果小于pivot,则right指针停止移动,切换到left指针。

在当前数列中,1<4,所以right直接停止移动,换到left指针,进行下一步行动。

轮到left指针行动,让指针所指向的元素和基准元素做比较,如果小于或等于pivot则指针向右移动,如果大于pivot,则left指针停止移动。

由于left开始指向的是基准元素,判断肯定想等,所以left右移一位。

由于7>4,left指针在元素7的位置停下。这时,让left和right指针所指向的元素进行交换。

接下来,进入第二次循环,重新切换到right指针,向左移动,right指针先移动到8,8>4,继续左移动。由于2<4,停在2的位置。

按照这个思路,后续步骤如图所示。

代码实现

使用双边循环法实现快速排序,采用递归方式实现。


public static void quickSort(int[] arr,int startIndex,int endIndex){
  //递归结束条件,startIndex 大于或等于endIndex时
  if(startIndex>=endIndex){
    return;
  }
  
  //得到基准元素的位置
  int pivotIndex=partition(arr,startIndex,endIndex);
  
  //根据基准元素,分成两部分进行递归排序
  quickSort(arr,startIndex,pivotIndex-1);
  
  quickSort(arr,pivotIndex+1,endIndex);
}
/**
* 分治法(双边循环法)
* param arr. 待交换的数组
* param startIndex. 起始下标
* param endIndex.   结束下标
*/
public static int partition(int[] arr,int startIndex,int endIndex){
  
  // 取第一个位置(也可以采用随机位置)d的元素作为基准元素
  int pivot =arr[startIndex];
  int left=startIndex;
  int right=endIndex;
  
  while(left!=right){
    // 控制right指针比较并向左移动
    while(left<right && arr[right]>pivot){
      right--;
    }
    //控制left指针比较并右移动
    while(left<right &&  arr[left]<=pivot){
      left++;
    }
    //交换left 和right 指针所指向的元素
    if(left<right){
      int p =arr[left];
      arr[left]=arr[right];
      arr[right]=p;
    }
  }
  // pivot和指针重合点交换
  arr[startIndex]=arr[left];
  arr[left]=pivot;
  return left;
}

public static void main (String[] args){
  int[] arr= new int[]{4,4,6,5,3,2,8,1};
  quickSort(arr,0,arr.length-1);
  System.out.println(Arrays.toString(arr));
}

在上述代码中,quickSort()通过递归方式,实现了分而治之的思想。

Partition()则实现了元素的交换,让数列中的元素依据自身大小,分别交换到基准元素的左右两边。

单边循环法

双边循环法从数组的两边交替遍历元素,虽然更加直观,但代码实现相对繁琐。而单边循环法则简单得多,只从数组的一边对元素进行遍历和交换。

给出原始数列如下,要求对其从小到大进行排序。

开始和双边循环法相似,首先选定基准元素pivot.同时设置一个mark指针指向数列起始位置,这个mark指针代表小于基准元素的区域边界

接下来,从基准元素的下一个位置开始遍历数组。

如果遍历到的元素大于基准元素,就继续往后遍历,如果遍历到的元素小于基准元素,则需要做两件事:

  1. 把mark指针右移动一位,因为小于pivot的区域边界增大了,
  2. 让最新遍历到的元素和mark指针所在位置的元素交换位置,因为最新遍历的元素归属于小于pivot的区域。

首先遍历到元素7,7>4,所以继续遍历。

接下来遍历到的元素是3,3<4,所以mark指针右移一位。

随后,让元素3和mark指针所在位置的元素交换,因为元素3归属于小于pivot的区域。

按照这个思路,继续遍历,后续步骤如图所示。

代码实现


public static void quickSort(intp[] arr,int startIndex,int endIndex){
   
  // 递归结束条件: startIndex 大于或等于 endIndex时
  if(startIndex >=endIndex){
    return;
  }
  // 得到基准元素位置
  int pivotIndex =partition(arr,startIndex,endIndex);
  
  //根据基准元素,分两部分进行递归排序
  quickSort(arr,startIndex,pivotIndex-1);
  quickSort(arr,pivotIndex+1,endIndex);
}


/**
* 分治(单边循环)
* param arr  待交换的数组
* param startIndex 起始下标
* param endIndex 结束下标
*/
private static int partition(int[] arr,int startIndex,int endIndex){
  
  // 取第一个位置(也可以选择随机位置)的元素作为基准元素
  int pivot= arr[startIndex];
  int mark= startIndex;
  
  for(int i=startIndex+1;i<=endIndex;i++){
    if(arr[i]<pivot){
      mark++;
      int p=arr[mark];
      arr[mark]=arr[i];
      arr[i]=p;
    }
  }
  arr[startIndex]=arr[mark];
  arr[mark]=pivot;
  return mark;
}

public static void main(String[] args){
  
  int[] arr=new int[]{4,4,6,5,3,2,8,1};
  quickSort(arr,0,arr,length-1);
  System.out.println(Arrays.toString(arr));
}
非递归实现


public static void quickSort(int[] arr,int startIndex,int endIndex){
  
  // 用一个集合栈来代替递归的函数栈
  Stack<Map<String,Integer>> quickSortStack=new Stack<Map<String,Integer>>();
  
  // 整个数列的起止下标,以哈希的形式入栈
  Map rootParam=new HashMap();
  
  rootParam.put("startIndex",startIndex);
  rootParam.put("endIndex",endIndex);
  quickSortStack.push(rootParam);
  
  //循环结束条件: 栈为空时
  while(!quickSortStack.isEmpty()){
    
    // 栈顶元素出栈,得到起止下标
    Map<String,Integer> param=quickSortStack.pop();
    //得到基准元素位置
    
    int pivotIndex=partition(arr,param.get("startIndex"),param.get("endIndex"));
    //根据基准元素分成两部分,把每一部分的起止下标入栈
    if(param.get("startIndex")<pivotIndex-1){
      
      Map<String,Integer> leftParam=new HashMap<String,Integer>();
      
      leftParam.put("startIndex",param.get("startIndex"));
      leftParam.put("endIndex",pivotIndex-1);
      quickSortStack.push(leftParam);
    }
    if(pivotIndex+1<param.get("endIndex")){
      Map<String,Integer> rightParam=new HashMap<String,Integer>();
      
      rightParam.put("startIndex",pivotIndex+1);
      rightParam.put("endIndex",param.get("endIndex"));
      quickSortStack.push(rightParam);
    }
  }
}

/**
* 分治 (单边循环)
* param arr  待交换的数组
* param startIndex 起始下标
* param endIndex  结束下标
*/
private static int partition(int[] arr,int startIndex,int endIndex){
  
  //取第一个位置为基准元素
  int pivot=arr[startIndex];
  int mark=startIndex;
  
  for(int i=startIndex+1;i<=endIndex;i++){
    if(arr[i]<pivot){
      mark++;
      int p =arr[mark];
      arr[mark]=arr[i];
      arr[i]=p;
    }
  }
  arr[startIndex]=arr[mark];
  arr[mark]=pivot;
  return mark;
}


public static void main(String[] args){
  
  int[] arr= new int[]{4,7,6,5,3,2,8,1};
  quickSort(arr,0,arr.length-1);
  System.out.println(Arrays.toString(arr));
}

和刚才递归实现相比,非递归方式代码的变动只发生在quickSort方法中。该方法引入了一个存储Map类型元素的栈,用于存储每一次交换时的起始下标和结束下标。每次循环,都会让栈顶元素出栈,通过partition()方法进行分治,并且按照基准元素的位置分成左右两部分,左右两部分再分别入栈。当栈为空时,说明排序已经完毕。推出循环。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值