快速排序(分治思想)

    快速排序:
     空间复杂度 :O(logn)-O(n)
     时间复杂度 :O(nlogn)-O(n^2)
     稳定性: 快速排序是不稳定的排序算法    2 2 1  排完之后相对顺序发生了变化
     只适用于数组排序。 

前面提到的 直接插入排序、冒泡排序、希尔排序、在元素有序或者说基本有序的情况下,时间复杂度为O(n).

而快速排序跟其他的几种排序算法恰恰相反, 快速排序在元素有序或者逆序的情况下,时间复杂度最糟糕为O(n^2).而在元素随机、或者说找的元素接近基准元素时、此种情况下时间复杂度能达到O(nlogn)量级。

所以针对快速排序有两种优化思路:

        1.取待排序数组中 头、中、尾三个位置的元素、取中间值作为基准元素。(此种方式最好,递归调用栈的深度最低)

        2.随机选一个元素作为基准元素。

还有一个问题,就是当数组中存在大量重复元素时,大量的重复元素被分到了基准的一边,导致数组严重失去平衡,这样会导致递归调用栈深度增加,从而导致快排的时间复杂度变为O(n^2).

针对此种问题的优化思路:

        1.使用两路快排进行优化(此种方式会将相同元素均衡的分配到数组两边)

 两路快排虽然不会导致数组失衡,但是针对大量重复元素进行排序,显然没有必要。并且也会增加递归调用栈的深度。

针对此种问题的优化思路:

        1.三路快排(只针对非重复元素进行排序,对重复元素不予理睬)。

所以快排的终极思路是:

        1.利用随机数、头中尾取中元素法保证针对有序或者基本有序序列,快排的时间复杂度不至于退化为O(n^2).

        2.采用三路快排解决数组中存在大量重复元素的问题。

代码展示:

#include<stdio.h>
#include<stdlib.h>
#define  random(x)    (rand()%x)
#include <time.h>

/*
	快速排序:
	 空间复杂度 :O(logn)-O(n) --> 空间复杂度本质上就是递归层数 
	 时间复杂度 :O(nlogn)-O(n^2)  --> 时间复杂地本质上是 O(n*递归层数) 
	 稳定性: 快速排序是不稳定的排序算法    2 2 1  排完之后相对顺序发生了变化
	 只适用于数组排序。 
	 要想知道快速排序的递归层数、那么首先我们首先就根据每次确定的基准以及左右子表,确定出一棵基准二叉树.该二叉树的深度就是递归的层数。
	 我们知道二叉树的深度h在 log2n - n 之前。 所以 快速排序的时间复杂度从O(n*logn)-O(n^2).
*/


 
//返回快速排序枢轴的下标
int partition(int a[],int low,int high){
	//第一个元素作为枢轴 
	int pivot = a[low];
	//用low、high搜索枢轴的最终位置 
	while(low < high){
		//从high所指元素往前找,找到一个比基准小的元素进行交换 
		while(low < high && a[high] >= pivot) high--;
		a[low]=a[high];
		//从low所指元素往后找,找到一个比基准大的元素进行交换 
		while(low < high && a[low]<= pivot) low ++;
		a[high]=a[low];
	}
	//枢轴元素存放到最终位置 
	a[low]=pivot;
	//返回存放枢轴的最终位置 
	return low;	
}



//交换两个下标中元素的值 
void swap(int a[],int index1,int index2){
	int temp = a[index1];
	a[index1] = a[index2];
	a[index2]=temp;
}

//知道为什么此种交换方式在快排中会出错了, 因为i可能会和j+1重合, 此时这种交换方式是不适用的,
/*
	1 12 43 28 16 23 32
	low 指向 43, j = 43 i从low下一个位置开始往后遍历   i = 28 小于 43 ,需要交换
	此时其实 j+1 和 i 指向的其实是一个位置,都是28。
	如果此时用这种交换方式
	a[j+1]= a[i]+a[j+1]=56;
	a[i]=[j+1]-a[i]=56-56=0;
	a[j+1]=a[j+1]-a[i]= 0-0=0;
	所以此种交换方式不可取 
*/ 
void swap2(int a[],int index1,int index2){
	a[index1]=a[index1]+a[index2];
	a[index2]=a[index1]-a[index2];
	a[index1]=a[index1]-a[index2];
} 

//快速排序的第二种思路: 一个下标搞定基准元素要插入的位置 
int partition2(int a[],int low ,int end){
	int j = low;
	int pivot = a[low];
	for(int i=low+1 ;i <= end;i++){
		//需要将元素移到前面去  (与基准元素相等的元素我们都放到了基准元素的左边) 
		if(a[i] <= pivot){
			swap(a,j+1,i);
			j++;
		}	
	} 
	//将枢轴元素放到正确的位置上
	swap(a,j,low);
	return j;
}

/*
	当数组中元素有序、基本有序时,快速排序的效率最低
	所以针对快速排序的优化:
	1.随机选待排数组中的一个元素作为基准元素。 
	2.选头、中、尾 三个元素、 选取中间的元素作为基准元素。
	这样即使当数组中元素有序、基本有序时,快速排序的递归调用栈不至于太深。
	 
*/ 
//随机选待排数组中的一个元素作为基准元素 
int partition3(int a[], int low ,int high){
	srand((int)time(NULL));
	int index = random((high-low+1));	
	//将index位置的元素交换到开头
	swap(a,low,low+index);
	
	int pivot = a[low];
	while(low<high){
		while(low<high && a[high]>=pivot) high--;
		a[low]=a[high];
		while(low<high && a[low]<= pivot) low++;
		a[high]=a[low];
	}
	a[low]=pivot;
	return low;
}

//选头、中、尾三个元素、选取中间的元素作为基准元素 
int partition4(int a[],int low,int high){
	int mid = (low+high)/2;
	//计算出三个数中的最大值
	int max=a[low] > a[mid] ? a[low]:a[mid];
		max=a[high]>max?a[high]:max;
	//计算出三个数中的最小值
	int min=a[low] < a[mid] ? a[low]:a[mid];
		min=a[high] <min ?a[high]:min;
	//中间值
	int middle = a[mid]+a[low]+a[high]-max-min;
	if(middle != a[low]){
		if(middle == a[mid]){
			swap(a,low,mid); 
		}else{
			swap(a,low,high);
		}
	}
	int pivot = a[low];
	
	int j = low;
	for(int i = low + 1; i <= high ; i++){
		if(a[i] <= pivot){
			swap(a,j+1,i);
			//j++不能少 
			j++;
		}
	}
	swap(a,j,low);
	return j;	
} 


/*
	如果排序序列中重复元素太多, 会导致一次分割会产生两个极不平衡的子序列。 --> 这样也会导致快排的递归调用栈太深。
 	导致算法的时间复杂度退化到O(n^2).
*/




//快速排序的进一步优化 (二路快排)  -->   将相同的元素均匀的分配到数组的两端 
int partition5(int a[], int low ,int high){
	int pivot = a[low];
	int i = low+1;
	int j = high;
 	while(true){
 		//a[i] >= pivot 结束循环 
	 	while( i <= high && a[i] < pivot) i++;
	 	//a[j] <= pivot 结束循环 
	 	while( j >= low+1 && a[j] > pivot) j--;
	 	if(i >= j){
	 		break;
	 	}
	 	swap(a,i,j);
	 	i++;
	 	j--;
	 }
	 swap(a,j,low);
	 return j;
}
//快速排序的更进一步的优化(三路快排)  --> 相同的元素不予处理  
int QuickSort2(int a[],int low ,int high,int *max,int *count){
	if(low < high){
		(*count)++;
		if((*max) < (*count)){
			(*max) = (*count);
		}	
	//让pivot的值尽量取中间元素,这样可以有效针对有序数组快排时间复杂度退化为O(n^2)的情况。(此种方式更高效,对于有序或者基本有序的数组,能使递归调用栈深度最低)
	int mid = (low+high)/2;
	//计算出三个数中的最大值
	int max1=a[low] > a[mid] ? a[low]:a[mid];
		max1=a[high]>max1?a[high]:max1;
	//计算出三个数中的最小值
	int min=a[low] < a[mid] ? a[low]:a[mid];
		min=a[high] <min ?a[high]:min;
	//中间值
	int middle = a[mid]+a[low]+a[high]-max1-min;
	if(middle != a[low]){
		if(middle == a[mid]){
			swap(a,low,mid); 
		}else{
			swap(a,low,high);
		}
	}
	
	//产生随机数的方式也可。 
//	srand((int)time(NULL));
//	int index = random((high-low+1));	
	//将index位置的元素交换到开头
//	swap(a,low,low+index);
	
		//找基准元素
		int pivot = a[low]; 
		//lt是小于等于的分界线(始终指向小于的最后一个位置) 
		int lt = low;
		//gt是大于等于的分界线, 也是遍历的终点 (始终指向大于的第一个位置)
		int gt = high+1; 
		int i = low+1; 
		while(i<gt){
			if(a[i] < pivot){
				swap(a,lt+1,i);
				lt++;
				i++;
			}else if (a[i] > pivot){
				swap(a,gt-1,i);
				gt--;
			}else{
				i++;
			}
		}
		swap(a,low,lt);
		QuickSort2(a,low,lt-1,max,count);
		QuickSort2(a,gt,high,max,count);
		(*count)--;
	}	
}

//a为要排序的数组   low为要排序数组第一个元素的下标, high为要排序数组最后一个元素的下标 
void QuickSort(int a[],int low,int high,int *max ,int *count){
	if(low < high){
		(*count)++;
		if((*max) < (*count)){
			(*max) = (*count);
		}
		//这个pivotpos把当前数组分成了两个子表 
		int pivotpos = partition4(a,low,high);
		//针对左子表找枢轴
		QuickSort(a,low,pivotpos-1,max,count);
		//针对右子表找枢轴 
		QuickSort(a,pivotpos+1,high,max,count);
		(*count)--;
	} 
}

int main(int argc, char *argv[])
{
	int a[] = {1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20};
	//int a[] = {49,49,56,49,49,49,49,49,49,49};
	//新增两个变量,用来记录调用栈的最大深度 
	int count = 0;
	int max = 0;
	QuickSort2(a,0,19,&max,&count);
	printf("调用栈的最大深度是%d\n",max);
	for(int i = 0 ; i < 20; i++ ){
		printf("%d ",a[i]);
	}
	return 0;
}

增加两个变量,记录递归调用栈的深度。

结果测试:

 

针对20个有序元素,采用最优的的快排代码, 递归调用栈最大深度是5.log20差不多也就是5.

针对大量重复元素的数组,采用最优的快排代码,递归调用栈最大深度为1。

结论: 

采用最优的快排代码,无论是数组有序、基本有序、还是说数组中存在大量的重复元素。

或者说数组中兼具这两个特点,快排代码都能保证递归调用栈的深度维持在log2n的水平。

也就是说使用最优的快排代码对任意数组进行排序,时间复杂度都能保持在O(nlogn),空间复杂度保持在O(logn)的水平。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值