算法(5) 快速排序 java

快速排序是对世界影响最大的排序算法之一,至于其背景之类的知识可百度获取.

思路如下:

假设待排序数组如下


现在要将4 放到4 应该放到的位置,快速排序的思想是将整个数组划分为两部分,一部分的数据都小于等于4,另一部分的数据都大于4,如下图:


继续抽象思路,观察上面的排列情况,假设待排序数组为[l,r]前闭后闭,[l+1,j]区间满足小于v,[j+1,i)前闭后开满足大于v,当前正在遍历的下标为i,如下图:


等所有的数据都按照上图排列之后,jl的元素进行交换,即将l对应的元素放到了应该放的位置,如下图:



扫描二维码关注公众号,回复: 4073748 查看本文章

理解了思路,那么代码会比较好实现,使用递归思想来实现快速排序:

/**
 * 快速排序 思想 V | <= V ||>V|
 * 
 * @author JamesWang Create on 2018年1月22日 下午1:55:10
 */
public class QuikSort {
	public static void sort(int[] arr) {
		quickSort(arr, 0, arr.length - 1);
	}

	/**
	 * [l,r]
	 * 
	 * @param arr
	 * @param i
	 * @param j
	 */
	private static void quickSort(int[] arr, int l, int r) {
		if (l >= r) {
			return;
		}
		int p = partition(arr, l, r);
		quickSort(arr, l, p - 1);
		quickSort(arr, p + 1, r);
	}
	/**
	 * 
	 * @param arr
	 * @param p1
	 * @param p2
	 */
	public static void swap(int[] arr,int p1,int p2) {
		if(p1 == p2 || arr[p1] == arr[p2]) {
			return;
		}
		arr[p1] = arr[p1] ^ arr[p2];
		arr[p2] = arr[p1] ^ arr[p2];
		arr[p1] = arr[p1] ^ arr[p2];
	}
	
	
	// [l+1,j] <= v [l+1,i) > v
	private static int partition(int[] arr, int l, int r) {
		int v = arr[l];
		int j = l;
		for (int i = l + 1; i <= r; i++) {
			if (arr[i] < v) {// j + 1 与 i 交换
				swap(arr,++j,i);			
			}
		}
		swap(arr, l, j);
		return j;
	}

	public static void main(String[] args) {
		int[] arr = SortUtils.createArray(20);
		System.out.println("arr before = " + SortUtils.getString(arr));
		sort(arr);
		System.out.println("arr after = " + SortUtils.getString(arr));

	}

}

但是上面的代码还有一些明显的不足,考虑一下此场景,当原数据本身就是接近于有序或者就是有序的情况下,此时该算法会退回到O(N2),在每一次进行递归的时候,都是直接从第二个元素开始都算作后半段,那么此时算法相当于一个链表此时复杂度为O(N),而每次的partition又是O(N),因此此时的时间复杂度为O(N2),下图为说明:


针对这个缺陷,在原有基础上还是比较好改善的,主要是在partition时进行优化

// [l+1,j] <= v [l+1,i) > v
	private static int partition(int[] arr, int l, int r) {
		int k = random.nextInt(r - l) + l + 1;//随机获取待排序的下标,然后将该下标对应的元素与l对应的元素交换
		swap(arr,k,l);
		//下面不变
		int v = arr[l];
		int j = l;
		for (int i = l + 1; i <= r; i++) {
			if (arr[i] < v) {// j + 1 与 i 交换
				swap(arr,++j,i);			
			}
		}
		swap(arr, l, j);
		return j;
	}

通过上面的改进,虽然不能百分百避免退化为O(N2)的情况,但是已经将这个概率降低为1/ (N * (N-1) * (N - 2) ... * 1) ,随着数据的增大,这个概率会无限接近于0.

现在这个快速排序优化了一个地方,但是当面对有多个重复值得情况时,这个快速排序依然会退化到O(N2),因为在上面的算法实现中,如果有多个重复值得话,会在算法中都分配到同一边,这样又相当于有序了,依然会退化为O(N2),这个时候是需要对重复元素进行处理,下面为双路快排,原理:从两边分别进行遍历,直到两边同时满足条件时才进行交换,具体partition代码如下:

private static int partition(int[] arr, int l, int r) {
		int k = random.nextInt(r - l) + l + 1;// 随机获取待排序的下标,然后将该下标对应的元素与l对应的元素交换
		swap(arr, k, l);
		int v = arr[l];
		int lk = l + 1;
		int rk = r;
		while (true) {
			while (lk <= rk && arr[lk] < v) {
				lk++;
			}
			while (rk >= lk && arr[rk] > v) {
				rk--;
			}
			if (lk > rk) {
				break;
			}
			swap(arr, lk, rk);
			lk++;
			rk--;
		}
		swap(arr, l, rk);
		return rk;
	}

除了双路快排还有一种改进过的快速排序算法值得借鉴,三路快速排序,整个算法中基本的逻辑不变,改变的是在partition的时候是怎么处理的,首先V是待排元素,核心逻辑:把数组划分为三部分,第一部分为小于V(在数组左侧),第二部分为等于V(在第一部分右边),第三部分为大于V(在数组右侧).详细看下面

/**
	 * 三路快排
	 * v 待排元素
	 * lk  [l+1,lk] < v  小于v的边界 在数组的左边
	 * i   [lk+1,i) = v 待排元素的下标 同时要保证 lk + 1 到i-1 为 == v
	 * rk  [rk , r] > v 大于v的边界 在数组的右边
	 * 	
	 * @param arr
	 * @param l
	 * @param r
	 */
	private static void quickSortThreeWays(int[] arr,int l,int r) {
		if(l >= r) {//其实这里可以优化为 当元素个数为比较少的时候 直接进行插入排序 即可 
			return;
		}
		
		int k = random.nextInt(r - l) + l + 1;//随机化 待排下标 以保持排序树的平衡 避免退化为O(N^2)
		swap(arr,l,k);
		
		int v = arr[l];
		
		int lk = l;
		int i = l + 1;
		int rk = r + 1;
		
		for(;i < rk;) {
			if(arr[i] > v) {
				swap(arr,i,--rk);
			}
			else if(arr[i] < v) {
				swap(arr,i,++lk);
				i++;
			}
			else if(arr[i] == v) {
				i++;
			}
		}
		swap(arr,l,lk);//将待排元素与小于v的最后一个进行交换 
		quickSortThreeWays(arr,l,lk - 1);//递归 排序 小于v的部分
		quickSortThreeWays(arr,rk,r);//递归排序大于v的部分
	}

这个的效率在面对有很多重复元素时,其效率会比普通的快速排序和双路快排要好,面对有很多元素并且没有那么多的重复元素时,其效率也不差,因此这个三路快排是快速排序中比较平衡的算法,三路快排是默认的语言库函数中使用的排序算法(比如java).






猜你喜欢

转载自blog.csdn.net/weixin_37699212/article/details/79142744