快速排序以及优化详解

为什么要用快速排序

场景

IP地址查询,例如1000w条IP,你的业务场景需要高频的进行数据查询,返回IP地址
这里防止杠精说 用线性回归预测IP地址 比二分法更高效 ,你你最牛批 用不着学快排这垃圾算法赶紧走
一般查询的话思路都是用二分法, 那么二分法的前提有三个, 1.数据是有序的 2. 数据中元素不能全部一样 3.长度不能为0  附加一个,如果数据中有重复元素,那么即使查询到 也不能确保是第几个相同元素
具体实现思路为
1.将 IP地址的4个0-255 十进制数 数转为十进制或者2进制
 x1.x2.x3.x4
 这里使用的是Java语言 c/c++都可以,更好理解位运算
 一个int类型数据占用4字节一字节占用8bit  正好是32位 ,
 而0-255 表示范围为 256  那么正好是2^8次方 用二进制理解就是8bit存储,每个数字占8位 
 四个数字就是32位置,恰好大小与int类型内存占用一致,int 类型表示范围为 -2147483648-2147483647
 直接使用位运算即可
 x1 左移 3*8 24位
 x2 左移 2*8 16位
 x3 左移 1*8 8 位
 x4 左移 0*8 0 位
然后令x1+x2+x3+x4 得出的结果就是IP对应的二进制数	,这里称为X
有了大X,也就是对应IP的映射后,就方便开始排序了 当然解码的时候翻过来解出对应的十进制数就可以了
2.处理完 开始排序
使用快排 能大幅度提高排序效率
3.排序完毕后持久化或者保存在内存中
4.二分法查询

当数据量很大时,使用这两种优秀的算法能大幅度提高效率

核心思想

在这里插入图片描述
在这里插入图片描述
比较简单 实现步骤概述一下
1.任意找一个基准值
2.从左往右开始找,找到第一个比基准值大的或者等于基准值的值,左指针停止,
3.开始从右往左找,找到第一个比基准值小的或者等于基准值的值,右指针停止
4.交换左右基准值 如此循环下去,直到 左右指针相等,此时左边没有一个值比基准值大,右边没有一个值比基准值小,那么就找到了此元素应该在的位置 终止循环,其实此时基准值已经到了自己应该存在的位置了
5. 基准值左边的 数列和基准值 右边的数列 进行递归 重复上述步骤
6. 递归终止条件 左指针大于右指针 或者等于右指针时停止循环
实践中此种思路存在一种问题,也就是重复元素存在时会无限递归,这个时候需要打破循环 ,当左指针元素等于右指针元素时 让任意一个指针位移一位即可
另外一种方法也类似, 将基准值空出来,从右边先开始找,找到小于基准值的值后,将此值写入到基准值位置,然后从左边开始找,找到小于基准值的值,将这个值写入到右指针位置, 直到两个指针重合时结束循环 ,将之前基准值写入到指针重合下标位置 ,然后分别递归 这个方法不会遇到相同元素的问题

代码实现

package com.sort;

import java.security.Principal;

public class QuickSort {
	/**
	 * 递归快速排序
	 * 
	 * 1.指定中间值 
	 * 2.从左边开始比较中间值,找到比中间值小的 
	 * 3.从右边开始比较,找到比中间值大的 4.交换左右两个下标的元素位置
	 * 5.交换比较终止条件 start=end时,找到了mid准确的位置 
	 * 6.交换mid准确位置的元素和mid元素位置 
	 * 7.此mid左边的元素和右边的元素 分别执行递归 8.左边递归 start - (mid-1) 右边递归 (mid+1) - end
	 */
	public static void quickSort(int[] arr, int start, int end) {
		if (start>end) {
			return;
		}
		//中间变量存储指针原始位置
		int start_p=start;
		int end_p=end;
		//1.指定中间值
		int mid=start;
		//2开始比较循环
		while(start_p< end_p){
			//每次找到左右两指针的值后,交换位置完毕,指针需要恢复如初
			//3左边开始比较
				if (arr[start_p]==arr[end_p]) {
					start_p++;
				}
			while(arr[start_p]<arr[mid] &&start_p<end_p){
				//找到一个大于基准值的数时,并且这个值和右指针不相等时才能结束//&& arr[start_p]==arr[end_p] 
			 			start_p++;
			 }
			System.out.print("本次循环左指针下标为:"+start_p);
			//右边开始比较
			while (arr[end_p]>arr[mid]   && start_p<end_p ) {
				end_p--;				
			 }
			System.out.println("	本次循环右指针下标为:"+end_p);
			 //交换位置,交换位置前,要确认指针是否是一个元素
				int tmp=arr[start_p];
				arr[start_p]=arr[end_p];
				arr[end_p]=tmp;
				for (int i = 0; i < arr.length; i++) {
					System.out.print(arr[i]+"	");
				}
			} 
			for (int i = 0; i < arr.length; i++) {
				System.out.print(arr[i]+"	");
			}
			System.out.println();
		//确定mid准确位置
		int mid_tmp=arr[start_p];
		arr[start_p]=arr[mid];
		arr[mid]=mid_tmp;
		//开始递归
		 quickSort(arr, start, start_p-1);
		 quickSort(arr, start_p+1, end);
		}
	
	/**
	 * 从右边开始找
	 * @param arr
	 * @param start
	 * @param end
	 */
	public static void quickSort2(int[] arr, int start, int end) {
		if (start>end) {
			return;
		}
		//中间变量存储指针原始位置
		int start_p=start;
		int end_p=end;
		//1.指定中间值
		int mid=start;
		//2开始比较循环
		while(start_p< end_p){
			//每次找到左右两指针的值后,交换位置完毕,指针需要恢复如初
			//3左边开始比较
				if (arr[start_p]==arr[end_p]) {
					start_p++;
				}
				while (arr[end_p]>arr[mid]   && start_p<end_p ) {
					end_p--;				
				 }
				System.out.println("	本次循环右指针下标为:"+end_p);
				while(arr[start_p]<arr[mid] &&start_p<end_p){
				//找到一个大于基准值的数时,并且这个值和右指针不相等时才能结束//&& arr[start_p]==arr[end_p] 
			 			start_p++;
			 }
			System.out.print("本次循环左指针下标为:"+start_p);
			//右边开始比较
		
			 //交换位置,交换位置前,要确认指针是否是一个元素
				int tmp=arr[start_p];
				arr[start_p]=arr[end_p];
				arr[end_p]=tmp;
				for (int i = 0; i < arr.length; i++) {
					System.out.print(arr[i]+"	");
				}
			} 
			for (int i = 0; i < arr.length; i++) {
				System.out.print(arr[i]+"	");
			}
			System.out.println();
		//确定mid准确位置
		int mid_tmp=arr[start_p];
		arr[start_p]=arr[mid];
		arr[mid]=mid_tmp;
		//开始递归
		 quickSort(arr, start, start_p-1);
		 quickSort(arr, start_p+1, end);
		}
	public static void main(String[] args) {
			int arr[]={6,2,6,2,932,5,123,10,932	};
			for (int i = 0; i < arr.length; i++) {
				System.out.print(arr[i]+"	");
			}
			quickSort2(arr, 0, arr.length-1);
//			quickSort(arr, 0, arr.length-1);
			System.out.println();
			for (int i = 0; i < arr.length; i++) {
				System.out.print(arr[i]+"	");
			}
		}
	}

算法评价

时间复杂度
最好情况
最好情况是当数组完全无序时,每个基准值都选在了中间位置 这样递归次数是最少的,类似二分法 时间复杂度为 1.39NlgN
平均情况
当数组完全无序时,每个基准值按照正态分布落在整个数组中,那么 算法的平均时间复杂度为 2NlgN
最坏情况
最坏情况分两种 , 第一种 是数组有序,那么每一刀都切在了边缘 其时间复杂度近似为 N*N
第二种 是数组完全无序,但偏偏每一刀都切在了边缘上,其时间复杂度一样为 N*N跟冒泡插入一样都是两层循环

空间复杂度O(1) 使用的额外空间与问题规模无关

那么快排的性能如何呢?
快排的平均情况之比最好情况 多39% 那么只损失了11%的性能,就能高效解决大部分场景的问题,足以说明这个算法的全面性

算法调优在这里插入图片描述

思路1.
一个已知结论:当快排计算中产生了大量的小数组时,会影响到一些算法的性能
那么如何更好的解决这个问题呢?
实践证明 当 小数组长度在5-15之间时 使用插入排序效果会很好
那么已知结论后,我找了找这个结论是如何推到出来的,但是貌似并没有人来解释这个问题,这个显然是一个数学问题 概率论
试图证明
逆向思维
1.插入排序的算法评价:
最好情况 O(N)
最坏情况 O(N*N)
平均情况 O(N*N)
平均来说,A[1…j-1]中的一半元素小于A[j],一半元素大于A[j]。插入排序在平均情况运行时间与最坏情况运行时间一样,是输入规模的二次函数 [1] 。
当插入排序中某个元素的组边都小于它,右边都大于它时它的时间复杂度不变, 而这种情况恰恰是快速排序最怕遇到的有序情况,这种情况会造成快排的时间复杂度为 N*N 跟插入排序的最坏情况一样
那么如果我们切换到插入排序有什么好处呢?
如果小数组已经是有序的,插入排序的复杂度是线性的 ,看图中的蓝色部分
效率明显比在这里插入图片描述
快速排序高
2.已经说明插入排序的好处下, 试图探讨 [5-15]这个区间是如何计算出来的
最简单的思路是让插入的最坏情况 时间复杂度等于快速排序时间复杂度的最坏情况
从图像上看,插入排序抛物线是有一部分是低于快排的曲线的,但是显然交点是小于5的,这里基本就无法往下证明结论
3.如果这样还不行,就只能从实际角度考虑问题,算法复杂度是忽略了低量级的影响因素,这种忽略在问题规模很大的时候是无感的,但是当遇到数量级很小时,问题往往就会发生,因为问题规模很小时,即使每次循环多比较了一次,都会严重影响性能.那么来看看插入排序的代码

public static void insertsort(int[] array){
        //array[0]作为哨兵项
        int i,j=0;
        for(i=2;i<array.length;i++){
            if(array[i]<array[i-1]){
                array[0] = array[i];
                array[i] = array[i-1];
                for(j=i-2;array[j]>array[0];j--){
                    array[j+1] = array[j];
                }
                array[j+1] = array[0];
                array[0] = 0;
            }
        }
    }
								插入排序:
									循环次数 N\*N
									比较次数 N\*N
									赋值次数 N\*N\*3+N
								快速排序
									循环次数 N\*N
									比较次数N\*N\*4+N
									赋值次数2N\*N+3logN+3N
								插入排序加和 4N\*N + N
								快速排序加和7N\*N+6N+3logN
				这里上述估计的次数不是十分准确,但是可以看到其他被忽略的量级和之间的影响,粗略的估算
				当计算规模很小的时候,不可忽略这些低量级时,插入排序确实比快速排序高
				同时 插入排序在低量级时出现最好情况的概率很大,而有序的情况恰恰对快速排序很致命.并且在数组切分的很小的时候,有很高的概率出现数组有序,或者是局部有序,这种情况还是很好理解.那么5-15到底是如何计算出来的呢?
			    本人太会准确计算每种算法准确的时间复杂度,因为其中有内存读写,交换比较等,如果可以准确计算,那可以得到一个准确的区间,然后在区间中取整数就可以了

3.当实际场景中出现大量重复元素时,使用熵最优排序
人话讲,将数组切分成3份,分别为小于基准值,等于基准值,大于基准值,剩余思路一致,只是将等于基准值的元素插进去,对应的下标指针需要相应的修改,但是再次递归的时候,只把小于基准值部分和大于基准值部分的数组传递过去,等于基准值的数组不用计算因为找到第一个正确的位置后,无论有多少个重复元素,都不需要改变位置
4.三取样切分
使用子数组中的一小部分元素的中位数来切分数组,这样的效果更好,但是代价是需要计算中位数,当大小为3时效果最好.也就是说不再将基准值设置为start,而是单独计算一个start-strat+3 数组的中位数,然后进行切分

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值