快速排序的4种优化

9 篇文章 10 订阅
订阅专栏
6 篇文章 5 订阅
订阅专栏

快排思想

快排基准的选择

          固定基准

          随机基准

          三数取中

快速排序的优化

优化1:序列长度达到一定大小时,使用插入排序

优化2:尾递归优化

优化3:聚集元素

优化4:多线程处理快排


快排思想

快排算法是基于分治策略的排序算法,其基本思想是,对于输入的数组 a[low, high],按以下三个步骤进行排序。

(1)分解:以 a[p] 为基准将 a[low: high] 划分为三段 a[low: p-1],a[p] 和 a[p+1: high],使得 a[low: p-1] 中任何一个元素小于等于 a[p], 而 a[p+1: high] 中任何一个元素大于等于 a[p]。

(2)递归求解:通过递归调用快速排序算法分别对 a[low: p-1] 和 a[p+1: high] 进行排序。

(3)合并:由于对 a[low: p-1] 和 a[p+1: high] 的排序是就地进行的,所以在 a[low: p-1] 和 a[p+1: high] 都已排好序后,不需要执行任何计算,a[low: high] 就已经排好序了。

快排基准的选择

快速排序的运行时间与划分是否对称有关。最坏情况下,每次划分过程产生两个区域分别包含n-1个元素和1个元素,其时间复杂度会达到O(n^2)。在最好的情况下,每次划分所取的基准都恰好是中值,即每次划分都产生两个大小为n/2的区域。此时,快排的时间复杂度为O(nlogn)。所以基准的选择对快排而言至关重要。快排中基准的选择方式有以下三种。

(1)固定基准

以下Partition()函数对数组进行划分时,以元素x = a[low]作为划分的基准。

template <class T>
int Partition(T a[], int low, int high)
{
    int i = low, j = high+1;
    T x = a[low];
    while(true)
    {
        while(a[++i] < x && i < high);
        while(a[--j] > x);
        if(i >= j)break;
        Swap(a[i], a[j]);
    }
    a[low] = a[j];
    a[j] = x;
    return j;
}

快排过程一趟:

快排动图(网上找的动图,其中有一个基准为 6 的标识错误。虽然基准选择方法不一样,但排序过程还是一样的):

如果数组元素是随机的,划分过程不产生极端情况,那么程序的运行时间不会有太大的波动。如果数组元素已经基本有序时,此时的划分就容易产生最坏的情况,即快速排序变成冒泡排序,时间复杂度为O(n^2)。

例如:序列[1][2][3][5][4][6]以固定基准进行快排时。

第一趟:[1][2][3][5][4][6]

第二趟:[1][2][3][5][4][6]

第三趟:[1][2][3][5][4][6]

第四趟:[1][2][3][4][5][6]

程序中要用的函数:(1)C++可以使用以下方法产生随机数,而单纯的使用rand()%M产生的是伪随机数。

#define M 100
srand(time(0));
for(int i = 0; i < M; i++)
    a[i] = rand()%(M);

(2)方法一:获得程序片段运行的时间:

clock_t start_time = clock();
//程序代码片段
clock_t end_time = clock();
cout<<"Runing time is:"<<static_cast<double>(end_time-start_time)/CLOCKS_PER_SEC*1000<<"ms"<<endl;

方法二:获得程序片段运行的时间:

#include<sys/time.h>
int gettimeofday(struct timeval*tv,struct timezone *tz )
struct timeval
{ 
    long tv_sec;/*秒*/ 
    long tv_usec;/*微妙*/
};
struct timezone
{ 
    int tz_minuteswest;
    /*和greenwich时间差*/
    int tz_dsttime; 
};

例如:

int main()
{
    float Time=0;
    struct timeval start;
    struct timeval end;

    gettimeofday(&start,NULL); //gettimeofday(&start,&tz);结果一样
    //程序片段
    gettimeofday(&end,NULL);

    Time=(end.tv_sec-start.tv_sec)*1000000+(end.tv_usec-start.tv_usec);//微秒
    cout << Time <<endl;
    return 0;
}

完整代码如下:

#include <iostream>
#include <time.h>
#include <stdlib.h>
#include <cstdlib>
#define M 1000000
using namespace std;

	template <class T>
void Print(T a[], int n, int m)
{
	for(int i = n; i < m; i++)
	{
		cout << "[" << a[i] << "]";
	}
	cout <<endl;
}

	template <class T>
void Swap(T &a, T &b)
{
	T asd;
	asd = a;
	a = b;
	b = asd;
}

	template <class T>
int Partition(T a[], int p, int r)
{
	int i = p, j = r+1;
	T x = a[p];
	while(true)
	{
		while(a[++i] < x && i < r);
		while(a[--j] > x);
		if(i >= j)break;
		Swap(a[i], a[j]);
	}
	a[p] = a[j];
	a[j] = x;
	return j;
}

	template <class T>
void QuickSort(T a[], int p, int r)
{
	if(p < r)
	{
		int q = Partition(a, p, r);
		QuickSort(a, p, q-1);
		QuickSort(a, q+1, r);
	}
}

int a[M] = {0};
int main()
{
	srand(time(0));
	for(int i = 0; i < M; i++)
		//a[i] = i+1;            //设置升序数组
		a[i] = rand()%(M);       //设置随机数组
		//if(i < M/2-1)          //设置重复数组
		//	a[i] = 5;
		//else
		//	a[i] = 10;
        //在检验数据的时候可以用Print()函数将其打印出来
        //Print(a, 0, M);
	clock_t start_time = clock();
	QuickSort(a, 0, M-1);
	clock_t end_time = clock();
	cout<<"Runing time is:"<<static_cast<double>(end_time - start_time)/CLOCKS_PER_SEC*1000<<"ms"<<endl;
        //Print(a, 0, M);
	return 0;
}

p.s:(1)在Codeblocks下处理升序数组时,元素最好设置少一点。设置的太大可能会出现下图提示:

(2)重复数组中的元素值只有两个。

(3)随机数组(较多重复元素)的设置是:a[i] = rand()%(M/100);。

数据如下:

固定基准对升序数组的分割极其糟糕,排序时间特别长,所以只设置了10万个元素。

(2)随机基准

在待排数组有序或基本有序的情况下,选择使用固定基准影响快排的效率。为了解决数组基本有序的问题,可以采用随机基准的方式来化解这一问题。算法如下:

int Random(int a[], int low, int high)//在low和high间随机选择一元素作为划分的基准
{   
    srand(time(0));
    int pivot = rand()%(high - low) + low;
    Swap(a[pivot],a[low]); //把随机基准位置的元素和low位置元素互换
    return a[low];
}

此时,原来Partition()函数里的T x = a[low];相应的改为T x = Random(a, low, high);

虽然使用随机基准能解决待排数组基本有序的情况,但是由于这种随机性的存在,对其他情况的数组也会有影响(若数组元素是随机的,使用固定基准常常优于随机基准)。随机数算法(Sherwood算法)能有效的减少升序数组排序所用的时间,数组元素越多,随机数算法的效果越好。可以试想,上述升序数组中有10万个元素而且各不相同,那么在第一次划分时,基准选的最差的概率就是十万分之一。当然,选择最优基准的概率也是十万分之一,随机数算法随机选择一个元素作为划分基准,算法的平均性能较好,从而避免了最坏情况的多次发生。许多算法书中都有介绍随机数算法,因为算法对程序的优化程度和下面所讲的三数取中方法很接近,所以我只记录了一种方法的运行时间。

(3)三数取中

由于随机基准选取的随机性,使得它并不能很好的适用于所有情况(即使是同一个数组,多次运行的时间也大有不同)。目前,比较好的方法是使用三数取中选取基准。它的思想是:选取数组开头,中间和结尾的元素,通过比较,选择中间的值作为快排的基准。其实可以将这个数字扩展到更大(例如5数取中,7数取中等)。这种方式能很好的解决待排数组基本有序的情况,而且选取的基准没有随机性。

例如:序列[1][1][6][5][4][7][7],三个元素分别是[1]、[5]、[7],此时选择[5]作为基准。

第一趟:[1][1][4][5][6][7][7]

三数取中算法如下:

int NumberOfThree(int arr[],int low,int high)
{
	int mid = low + ((high - low) >> 1);//右移相当于除以2

	if (arr[mid] > arr[high])
	{
		Swap(arr[mid],arr[high]);
	}
	if (arr[low] > arr[high])
	{
		Swap(arr[low],arr[high]);
	}
	if (arr[mid] > arr[low]) 
	{
		Swap(arr[mid],arr[low]);
	}
	//此时,arr[mid] <= arr[low] <= arr[high]
	return arr[low];
}

同理,Partition()函数里的T x = a[low];相应的改为T x = NumberOfThree(a, low, high);

数据如下:

三数取中(随机数算法效果相同)在处理升序数组时有质的飞越,而且处理的还是100万个元素。

快速排序的优化

优化1:序列长度达到一定大小时,使用插入排序

当快排达到一定深度后,划分的区间很小时,再使用快排的效率不高。当待排序列的长度达到一定数值后,可以使用插入排序。由《数据结构与算法分析》(Mark Allen Weiness所著)可知,当待排序列长度为5~20之间,此时使用插入排序能避免一些有害的退化情形。

template <class T>
void QSort(T arr[],int low,int high)
{
    int pivotPos;
    if (high - low + 1 < 10)
    {
        InsertSort(arr,low,high);
        return;
    }
    if(low < high)
    {
        pivotPos = Partition(arr,low,high);
        QSort(arr,low,pivotPos-1);
        QSort(arr,pivotPos+1,high);
    }
}

完整代码如下:

/*
   次快排代码采用了  三数取中&插入排序
 */
#include <iostream>
#include <time.h>
#include <stdlib.h>
using namespace std;

#define M 1000000
int NumberOfThree(int arr[],int low,int high);

	template <class T>
void Print(T a[], int n)
{
	for(int i = 0; i < n; i++)
	{
		cout << "[" << a[i] << "]";
	}
	cout <<endl;
}

	template <class T>
void Swap(T &a, T &b)
{
	T asd;
	asd = a;
	a = b;
	b = asd;
}

	template <class T>
int Partition(T a[], int p, int r)
{
	int i = p, j = r+1;
	T x = NumberOfThree(a, p, r);
	while(true)
	{
		while(a[++i] < x && i < r);
		while(a[--j] > x);
		if(i >= j)break;
		Swap(a[i], a[j]);
	}
	a[p] = a[j];
	a[j] = x;
	return j;
}


void InsertSort(int arr[], int m, int n)
{
	int i, j;
	int temp; // 用来存放临时的变量
	for(i = m+1; i <= n; i++)
	{
		temp = arr[i];
		for(j = i-1; (j >= m)&&(arr[j] > temp); j--)
		{
			arr[j + 1] = arr[j];
		}
		arr[j + 1] = temp;
	}
}

int NumberOfThree(int arr[],int low,int high)
{
	int mid = low + ((high - low) >> 1);

	if (arr[mid] > arr[high])
	{
		Swap(arr[mid],arr[high]);
	}
	if (arr[low] > arr[high])
	{
		Swap(arr[low],arr[high]);
	}
	if (arr[mid] > arr[low]) 
	{
		Swap(arr[mid],arr[low]);
	}
	//此时,arr[mid] <= arr[low] <= arr[high]
	return arr[low];
}

	template <class T>
void QSort(T arr[],int low,int high)
{
	int pivotPos;
	if (high - low + 1 < 10)
	{
		InsertSort(arr,low,high);
		return;
	}
	if(low < high)
	{
		pivotPos = Partition(arr,low,high);
		QSort(arr,low,pivotPos-1);
		QSort(arr,pivotPos+1,high);
	}
}

int a[M] = {0};
int main()
{
	srand(time(0));
	for(int i=0;i<M;i++)
		//a[i] = i+1;              //设置升序数组
		a[i] = rand()%(M);         //设置随机数组
		//if(i < M/2-1)            //设置重复数组
		//	a[i] = 1;
		//else
		//	a[i] = 10;
	//Print(a, M);
	clock_t start_time = clock();
	QSort(a, 0, M-1);
	clock_t end_time = clock();
	cout<<"Runing time is:"<<static_cast<double>(end_time - start_time)/CLOCKS_PER_SEC*1000<<"ms"<<endl;
	//Print(a, M);
	return 0;
}

数据如下:

如上所述,在划分到很小的区间时,里面的元素已经基本有序了,再使用快排,效率就不高了。所以,在结合插入排序后,程序的执行效率有所提高。

优化2:尾递归优化

快排算法和大多数分治排序算法一样,都有两次递归调用。但是快排与归并排序不同,归并的递归则在函数一开始, 快排的递归在函数尾部,这就使得快排代码可以实施尾递归优化。使用尾递归优化后,可以缩减堆栈的深度,由原来的O(n)缩减为O(logn)。

尾递归概念:

如果一个函数中所有递归形式的调用都出现在函数的末尾,当递归调用是整个函数体中最后执行的语句且它的返回值不属于表达式的一部分时,这个递归调用就是尾递归。尾递归函数的特点是在回归过程中不用做任何操作,这个特性很重要,因为大多数现代的编译器会利用这种特点自动生成优化的代码。

尾递归原理:

当 编译器检测到一个函数调用是尾递归的时候,它就覆盖当前的活动记录而不是在栈中去创建一个新的。编译器可以做到这点,因为递归调用是当前活跃期内最后一条待执行的语句,于是当这个调用返回时栈帧中并没有其他事情可做,因此也就没有保存栈帧的必要了。通过覆盖当前的栈帧而不是在其之上重新添加一个,这样所使用的栈空间就大大缩减了,这使得实际的运行效率会变得更高。

代码如下:

#include <bits/stdc++.h>
using namespace std;

int fact(int n)             //线性递归
{
    if (n < 0)
        return 0;
    else if(n == 0 || n == 1)
        return 1;
    else
        return n * fact(n - 1);
}

int facttail(int n, int a)   //尾递归
{
    if (n < 0)
        return 0;
    else if (n == 0)
        return 1;
    else if (n == 1)
        return a;
    else
        return facttail(n - 1, n * a);
}

int main()
{
    int a = fact(5);
    int b = facttail(5, 1);
    cout << "A:" << a <<endl;
    cout << "B:" << b <<endl;
}

示例中的函数是尾递归的,因为对facttail的单次递归调用是函数返回前最后执行的一条语句。在facttail中碰巧最后一条语句也是对facttail的调用,但这并不是必需的。换句话说,在递归调用之后还可以有其他的语句执行,只是它们只能在递归调用没有执行时才可以执行尾递归是极其重要的,不用尾递归,函数的堆栈耗用难以估量,需要保存很多中间函数的堆栈。比如f(n, sum) = f(n-1) + value(n) + sum; 会保存n个函数调用堆栈,而使用尾递归f(n, sum) = f(n-1, sum+value(n)); 这样则只保留后一个函数堆栈即可,之前的可优化删去。

代码当n=5时,线性递归的递归过程如下:

fact(5)
{5*fact(4)}
{5*{4*fact(3)}}
{5*{4*{3*fact(2)}}}
{5*{4*{3*{2*fact(1)}}}}
{5*{4*{3*{2*1}}}}
{5*{4*{3*2}}}
{5*{4*6}}
{5*24}
120

而尾递归的递归过程如下:

facttail(5,1)
facttail(4,5)
facttail(3,20)
facttail(2,60)
facttail(1,120)
120

关于尾递归及快排尾递归优化可以看这篇博文: 尾递归及快排尾递归优化 ,其中包含了上述阶乘问题、快排尾递归优化和Gdb调试等内容。

在Codeblocks里运行快排代码处理升序数组,一个进行尾递归优化,而另一个不变。没有使用尾递归的代码处理4万个数组元素时,由于超过了栈的深度,程序会异常结束。而使用了尾递归的代码,就算处理10万个数组元素,也不会出现异常(结合三数取中,可以处理100万个数组元素)。

2018年10月2日补充:结合我的另一篇博文 《内存四区》,对上述问题有更全面的认识。

快排尾递归代码如下:

template <class T>
void QSort(T arr[],int low,int high)
{
    int pivotPos;
    if (high - low + 1 < 10)
    {
        InsertSort(arr,low,high);
        return;
    }
    while(low < high)
    {
        pivotPos = Partition(arr,low,high);
        QSort(arr,low,pivotPos-1);
        low = pivotPos + 1;
    }
}

第一次递归以后,变量low就没有用处了, 也就是说第二次递归可以用迭代控制结构代替。快排尾递归过程如下,纵向是递归,横向是迭代。

数据如下:

对递归的优化,主要是为了减少栈深度。在处理随机数组时,(三数取中+插排+尾递归)的组合并不一定比(三数取中+插排)的效率高。

优化3:聚集元素

聚集元素的思想:在一次分割结束后,将与本次基准相等的元素聚集在一起,再分割时,不再对聚集过的元素进行分割。具体过程有两步,①在划分过程中将与基准值相等的元素放入数组两端,②划分结束后,再将两端的元素移到基准值周围。

普通过程例如:[7][2][7][1][7][4][7][6][3][8] 由三数取中可得基准为[7]

第一趟:[7] [2] [3] [1] [6] [4] [7] [7] [7] [8]

第二趟:[1] [2] [3] [4] [6] [7] [7] [7] [7] [8]

第三趟:[1] [2] [3] [4] [6] [7] [7] [7] [7] [8]

第四趟:[1] [2] [3] [4] [6] [7] [7] [7] [7] [8]

聚集相同元素:

第一步:[7] [7] [7] [1] [2] [4] [3] [6] [7] [8]

第二步:[6] [3] [4] [1] [2] [7] [7] [7] [7] [8]

接下来是对[6] [3] [4] [1] [2] 和 [8]进行快排。(具体过程可以按照以下代码走一遍)

template <class T>
void QSort(T arr[],int low,int high)
{
	int first = low;
	int last = high;

	int left = low;
	int right = high;

	int leftLen = 0;
	int rightLen = 0;

	if (high - low + 1 < 10)
	{
		InsertSort(arr,low,high);
		return;
	}

	//一次分割
	int key =  NumberOfThree(arr,low,high);//使用三数取中选择枢轴

	while(low < high)
	{
		while(high > low && arr[high] >= key)
		{
			if (arr[high] == key)//处理相等元素
			{
				Swap(arr[right],arr[high]);
				right--;
				rightLen++;
			}
			high--;
		}
		arr[low] = arr[high];
		while(high > low && arr[low] <= key)
		{
			if (arr[low] == key)
			{
				Swap(arr[left],arr[low]);
				left++;
				leftLen++;
			}
			low++;
		}
		arr[high] = arr[low];
	}
	arr[low] = key;

	//一次快排结束,把与基准相等的元素移到基准周围
	int i = low - 1;
	int j = first;
	while(j < left && arr[i] != key)
	{
		Swap(arr[i],arr[j]);
		i--;
		j++;
	}
	i = low + 1;
	j = last;
	while(j > right && arr[i] != key)
	{
		Swap(arr[i],arr[j]);
		i++;
		j--;
	}
        QSort(arr,first,low - 1 - leftLen);
        QSort(arr,low + 1 + rightLen,last);
}

聚集元素第一步:

聚集元素第二步:

下一次就是对[6] [3] [4] [1] [2] 进行快排。当划分区间达到插入排序的要求时,就使用插入排序完成后续工作,所以进入插入排序那一段代码是停止继续递归的标志。

数据如下:

从上表中可以看到,通过对快排聚集元素的优化,在处理数组中的重复元素时有很大的提升。而对于升序数组而言,因为其本身就是有序的,而且没有重复元素,所以结果没有(三数取中+插排)效率高。

优化4:多线程处理快排

分治法的基本思想是将一个规模为n的问题分解为k个规模较小的子问题,这些子问题互相独立且与原问题相同。求解这些子问题,然后将各子问题的解合并,从而得到的原问题的解。由此,在处理快排的时候,可以使用多线程提高排序的效率。

要使用的函数:
(1)pthread_create
创建一个线程的函数是pthread_create。其定义如下:

#include <pthread.h>//(Linux下编译需要加 -lpthread)
int pthread_create(pthread_t* thread, const pthread_attr_t* attr, void* (*start_routine)(void*), void *arg);

第一个参数是一个整数类型,它表示的是资源描述符,实际上,Linux上几乎所有的资源标识符都是一个整型数。第二个attr参数用于设置新线程的属性。给它传递NULL表示使用默认线程属性。start_routine和arg参数分别指定新线程将运行的函数及其参数。
pthread_create()成功时返回0,失败时返回错误码。

(2)pthread_barrier_init
多线程编程时,可以使用这个函数来等待其它线程结束,例如:主线程创建一些线程,这些线程去完成一些工作,而主线程需要去等待这些线程结束(pthread_join也能实现一种屏障)。可以把屏障理解为:为了协同线程之间的工作而使得某一具体线程进入等待状态的一种机制。其原型:

int pthread_barrier_init(pthread_barrier_t *restrict barrier, const pthread_barrierattr_t *restrict attr, unsigned int count);


函数执行成功返回 0,执行失败则返回一个错误号,我们可以通过该错误号获取相关的错误信息。
第一个参数:一个指向pthread_barrier_t 类型的指针,我们必须要指出的是pthread_barrier_init函数不会给指针分配相关内存空间,因此我们传入的指针必须为一个pthread_barrier_t 变量。
第二个参数:用于指定屏障的细节参数,我们这里可以暂且不去管它,如果我们传入NULL,那么系统将按照默认情况处理。
第三个参数:设计屏障等待的最大线程数目。

(3)pthread_barrier_wait
当一个线程需要等待其它多个线程结束时,调用该函数。
原型:

int pthread_barrier_wait(pthread_barrier_t *barrier);

函数执行成功返回 0,执行失败则返回一个错误码,我们可以通过该错误码获取相关的错误信息。
函数参数:指向pthread_barrier_t 变量的指针。
注意:使用barrier这个屏障,无法获取线程的结束状态。若想要获取相关线程结束状态,则需要调用pthread_join函数。

代码如下:

#include <cstdio> /*三数取中+插入+聚集元素+多线程组合  &&  三数取中+插入+尾递归+多线程组合*/
#include <iostream>
#include <stdlib.h>
#include <sys/time.h>
#include <pthread.h>
using namespace std;

const long MAX = 1000000L;                          //数组中最大数
const long long MaxNumber = 1000000L;               //排序数
const int thread = 4;                               //线程数
const long NumberOfSort = MaxNumber / thread;       //每个线程排序的个数

int array_a[MaxNumber];                         
int array_b[MaxNumber];                             //合并后,由b数组记录最终序列
 
pthread_barrier_t barrier;

void initial()   //数组初始化函数
{
		srand(time(0));
		for(int i = 0; i < MaxNumber; ++i)
                array_a[i] = rand()%(MAX);
		//if(i < MaxNumber/2)
		//    array_a[i] = 5;
		//else
		//   array_a[i] = 10;
		//array_a[i] = i+1;
}


		template <class T>
void Print(T a[], int n)
{
		for(int i = 0; i < n; i++)
		{
				cout << "[" << a[i] << "]";
		}
		cout <<endl;
}

		template <class T>
void Swap(T &a, T &b)
{
		T asd;
		asd = a;
		a = b;
		b = asd;
}

void InsertSort(int arr[],int start,int end)
{
	int low,high,median,tmp;
	for(int i = start+1;i<= end;i++)
	{
		low = start;
		high = i-1;

		while(low <= high)
		{
			median = (low + high) /2;
			if(arr[i] < arr[median])
			{
				high = median -1;
			}else
			{
				low = median + 1;
			}
		}

		tmp = arr[i];

		for(int j = i-1;j>high;j--)
		{
			arr[j+1] = arr[j];
		}
		arr[high+1] = tmp;
	}
}

int NumberOfThree(int arr[],int low,int high)
{
		int mid = low + ((high - low) >> 1);
		if (arr[mid] > arr[high])
		{
				Swap(arr[mid],arr[high]);
		}
		if (arr[low] > arr[high])
		{
				Swap(arr[low],arr[high]);
		}
		if (arr[mid] > arr[low])
		{
				Swap(arr[mid],arr[low]);
		}
		return arr[low];
}

		template <class T>
int Partition(T a[], int p, int r)
{
		int i = p, j = r+1;
		T x = NumberOfThree(a, p, r);
		while(true)
		{
				while(a[++i] < x && i < r);
				while(a[--j] > x);
				if(i >= j)break;
				Swap(a[i], a[j]);
		}
		a[p] = a[j];
		a[j] = x;
		return j;
}

#if 1   //有聚集元素的快排
		template <class T>
void QSort(T arr[],int low,int high)
{
		int first = low;
		int last = high;
		int left = low;
		int right = high;
		int leftLen = 0;
		int rightLen = 0;

		if (high - low + 1 < 6)
		{
				InsertSort(arr,low,high);
				return;
		}

		//一次分割
		int key = NumberOfThree(arr,low,high);//使用三数取中法选择枢轴

		while(low < high)
		{
				while(high > low && arr[high] >= key)
				{
						if (arr[high] == key)//处理相等元素
						{
								Swap(arr[right],arr[high]);
								right--;
								rightLen++;
						}
						high--;
				}
				arr[low] = arr[high];
				while(high > low && arr[low] <= key)
				{
						if (arr[low] == key)
						{
								Swap(arr[left],arr[low]);
								left++;
								leftLen++;
						}
						low++;
				}
				arr[high] = arr[low];
		}
		arr[low] = key;

		//一次快排结束
		//把与枢轴key相同的元素移到枢轴最终位置周围
		int i = low - 1;
		int j = first;
		while(j < left && arr[i] != key)
		{
				Swap(arr[i],arr[j]);
				i--;
				j++;
		}
		i = low + 1;
		j = last;
		while(j > right && arr[i] != key)
		{
				Swap(arr[i],arr[j]);
				i++;
				j--;
		}
		QSort(arr,first,low - 1 - leftLen);
		QSort(arr,low + 1 + rightLen,last);
}
#endif

#if 0     //没有聚集元素的快排
		template <class T>
void QSort(T arr[],int low,int high)
{
		int pivotPos;
		if (high - low + 1 < 10)
		{
				InsertSort(arr,low,high);
				return;
		}

		while(low < high)
		{
				pivotPos = Partition(arr,low,high);
				QSort(arr,low,pivotPos-1);
				low = pivotPos + 1;
		}
}
#endif // 1

void* work(void *arg)  //线程排序函数
{
		long length = (long)arg;
		QSort(array_a, length, length + NumberOfSort - 1);
		pthread_barrier_wait(&barrier);
		pthread_exit(NULL);
}

void meger()        //最终合并函数
{
		long index[thread];
		for (int i = 0; i < thread; ++i)
		{
				index[i] = i * NumberOfSort;
		}

		for(long i = 0; i < MaxNumber; ++i)
		{
				long min_index;
				long min_num = MAX;
				for(int j = 0; j < thread; ++j)
				{
				    if((index[j] < (j + 1) * NumberOfSort)&& (array_a[index[j]] < min_num))
						{
								min_index = j;
								min_num = array_a[index[j]];
						}
				}
				array_b[i] = array_a[index[min_index]];
				index[min_index]++;
		}
}


int main(int argc, char const *argv[])
{
		initial();
		//Print(array_a,MaxNumber);
		struct timeval start, end;
		pthread_t ptid;
		gettimeofday(&start, NULL);

		pthread_barrier_init(&barrier, NULL, thread + 1);
		for(int i = 0; i < thread; ++i)
				pthread_create(&ptid, NULL, work, (void *)(i * NumberOfSort));

		pthread_barrier_wait(&barrier);
		meger();

		gettimeofday(&end, NULL);
		long long s_usec = start.tv_sec * 1000000 + start.tv_usec;
		long long e_usec = end.tv_sec * 1000000 + end.tv_usec;

		double Time = (double)(e_usec - s_usec) / 1000.0;
		printf("sort use %.4f ms\n", Time);
		//Print(array_b, MaxNumber);
		return 0;
}


上传完这段代码,同学告诉我说,这段代码在Linux和Codeblocks里运行的时间不一样(本篇博文的数据都是在Codeblocks上测得的)。然后我立马就测试了一下,发现这之间存在误差,初步猜测是由于编译器引起的。由于我不是双系统,是在虚拟机上运行的Linux系统,这可能是造成误差原因之一(个人认为可以忽略误差,虽然每组数据在不同环境下平均运行时间有差距,但其整体优化的方向是不变的)。

数据如下:

从上表可以看出,结合了多线程的快排(三数+插排+多线程)在处理前三种数组时都有明显的提升。重复数组处理时间增加的原因是:聚集元素在处理重复数组时的表现已经很好了,因为在多线程的组合中,各个线程排完序后要合并,所以增加了(三数+插排+多线程)这一组合的排序时间。因为时间原因,以上的数据,是运行相应代码10次所取得平均值。如果想要得到更精确的数据,需要大量的运行上述代码(即使存在一些不稳定的数据,也不会影响到代码优化的方向)。PS.以上程序运行时间还与个人所使用的电脑配置有关。

参考:

快速排序 优化 详细分析_HermanLiu的博客-CSDN博客

三种快排及四种优化方式_silentsharer的博客-CSDN博客_快排csdn

尾递归_百度百科

多线程排序+快速排序_XD灬的博客-CSDN博客_多线程排序

【决战西二旗】|快速排序优化
kkkkk0826的专栏
12-02 420
1.前言 前面的一篇文章https://www.cnblogs.com/backnullptr/p/11934841.html讲了快速排序的基本概念、核心思想、基础版本代码实现等,让我们对快速排序有了一个充分的认识,但还无法达到面试中对快速排序灵活应对的程度。 快速排序是图领奖得主发明的算法,被誉为20世纪最重要的十大算法之一,快速排序为了可以在多数据集都有出色的表现,进行了非常多的优化,因...
快速排序优化
genzld的博客
11-15 608
快速排序以及快速排序优化: 一:快速排序的基本思想 快排使用分治的思想: 通过一趟排序将待排序序列分割成两部分,其中一部分记录的关键字均比另一部分记录的关键字小。之后分别对这两部分记录继续进行排序,以达到整个序列有序的目的。 二:快速排序的三个步骤 1.选择基准 在待排序列中,按照某方式挑出一个元素,作为“基准”(pivot) 2.分割操作 以该基准在序列中...
快速排序3实现方法及优化(详细图解)
weixin_61125362的博客
01-17 2135
快排(Quick sort)三的方法及优化:左右指针法,挖矿法,左右指针法;优化:三位取中 以及利用数据结构栈的非递归版,详细图解
数据结构详解】——快速排序(动图详解)
最新发布
HinsCoder的博客
08-13 2017
💡 算法思想:快速排序是Hoare于1962年提出的一**二叉树结构**的交换排序方法,通过一趟排序将待排记录分割成==独立的两部分==,其中一部分记录的关键字均比另一部分记录的关键字小,则可分别对这两部分记录继续进行排序,以达到整个序列有序的目的。
快速排序优化思路
m0_53157173的博客
04-10 927
在对快速排序进行优化前,先让我们回顾一些快速排序的思想: 快速排序就是分而治之思想的体现,将有序序列分成对立的两部分,一部分值都比关键字值小,一部分值都比关键字值大,再分别对两部分进行排序 对快速排序不了解的可以先看看快速排序的具体过程和代码讲解 下面来看代码: #include<iostream> #include<algorithm> using namespace std; //该函数作用:把比关键字小的放到关键字的左边,比关键字大的放到关键字右边 int partition(
快速排序详解及优化方式
2301_80636143的博客
04-27 2036
快速排序的时间复杂度是O(n log n),其中n为待排序数组的长度。快速排序的每次划分操作需要O(n)的时间复杂度,而递归调用的次数为O(log n),因此总的时间复杂度为O(n log n)。在最坏的情况下,即数组已经有序的情况下,快速排序的时间复杂度为O(n^2)。
快速排序优化的几方法代码实现
09-15
快速排序是一高效的排序算法...以上就是快速排序的几优化方法,它们在不同的场景下能提升快速排序的效率和稳定性。通过这些优化快速排序在实际应用中展现出强大的性能,成为许多编程语言内置排序函数的首选算法。
C++快速排序的分析与优化详解
09-04
快速排序是一高效的排序算法,由英国计算机科学家C.A.R. Hoare在1960年提出。它的核心思想是分治策略,通过选取一个主元(pivot)将数组分为两个子数组,使得一个子数组的所有元素都小于主元,而另一个子数组的...
快速排序优化方案
weixin_43899008的博客
02-20 6720
文章目录快速排序及其优化方案1. 快速排序的流程2. 快速排序的实现时间复杂度计算3. 快速排序优化随机获取基准值进行优化3.2二路快速排序原理:思想:4. 总结 快速排序及其优化方案 1. 快速排序的流程 首先设定一个分界值,通过该分界值将数组分成左右两部分。 将大于分界值的数据集中到数组右边,小于分界值的数据集中到数组的左边,而等于分分界值的部分放在相对中间的部分。此时,左边部分中各元素都小于分界值,而右边部分中各元素都大于分界值,相对中间的部分的的数据等于分界值。 然后,左边和右边的数据可以独立排序
Java编程中快速排序算法的实现及相关算法优化
09-02
快速排序是一高效的排序算法,由C.A.R. Hoare在1960年提出。它的基本思想是通过一趟排序将待排序的数据分割成独立的两部分,其中一部分的所有数据都比另一部分的所有数据都要小,然后再按此方法对这两部分数据分别...
快速排序算法C语言实现与优化.pdf
06-24
快速排序算法是一高效的排序算法,通过不断的优化和改进,其性能得到了显著提升。未来,随着计算机硬件和软件技术的不断进步,快速排序算法还有很大的发展空间,特别是在并行计算和分布式计算领域,快速排序算法...
快速排序及其优化
weixin_39034379的博客
09-13 3113
3.快速排序及其优化 **并没有最好的排序算法,只有最适合的排序算法。**在面对不同规模的数据和不同排列的数据的时候,都有着适合当前数据的排序算法,一个好的排序算法就是面对合适的场景去选取最适合它的排序算法。 3.1 普通快速排序 **快排的思想找到一个值使得数组左边的值都是小于等于它的数,数组的右边都是大于它的数。**然后继续去操作左右区间。可以看到,分成左右区间后,分别对不同的区间继续当前操作,非常适合递归。 如上图所示,需要两个指针。 j 数组遍历的位置,i 第一个大于等于 选取的基准值的位置。 遍
快速排序优化(一)
yuanopen
03-03 421
和归并排序做对比:归并排序时间复杂度:快速排序时间复杂度:快速排序最差时间复杂度:解决方法:随机化快排快速排序的最坏情况基于每次划分对主元的选择。基本的快速排序选取第一个元素作为主元。这样在数组已经有序的情况下,每次划分将得到最坏的结果。一比较常见的优化方法是随机化算法,即随机选取一个元素作为主元。这情况下虽然最坏情况仍然是O(n^2),但最坏情况不再依赖于输入数据,而是由于随机函数取值不佳。...
数据结构与算法】快速排序优化
Mr_Fmnwon的博客
03-20 1102
从快排的原理下手,我们能够对快速排序进一步优化。可以从中心值的选取、小规模直接插入排序、并行优化处理。在递归函数中采用多线程,是需要格外注意的点。
快速排序的5优化方法
热门推荐
qq_19525389的博客
08-05 2万+
快速排序以及快速排序优化 1、快速排序的基本思想:    快速排序使用分治的思想,通过一趟排序将待排序列分割成两部分,其中一部分记录的关键字均比另一部分记录的关键字小。之后分别对这两部分记录继续进行排序,递归地以达到整个序列有序的目的。 2、快速排序的三个步骤: (1)选择基准:在待排序列中,按照某方式挑出一个元素,作为 "基准"(pivot) (2)分割操作:以该基准在序列中...
快排的优化
枯树无花
05-05 621
https://www.cnblogs.com/c4isr/p/4603980.html https://blog.csdn.net/qq_19525389/article/details/81436838
优化快速排序 python
05-14
快速排序是一常见的排序算法,其时间复杂度为 O(nlogn),但是在处理大量数据的时候,它的效率可能不够高。以下是一些优化快速排序的方法: 1. 随机化选择基准值:快速排序的效率取决于基准值的选择,如果选择的基准值恰好是最小值或最大值,那么排序的效率将会很慢。随机选择基准值可以减少出现这情况的概率,提高排序的效率。 2. 三数取中法选择基准值:在数组的左端、右端和中间位置分别选取一个数,取它们的中位数作为基准值,可以减少基准值选择不当的情况。 3. 插入排序:当待排序的数组长度小于一定阈值时,使用插入排序可以提高排序的效率。 4. 尾递归优化:将递归过程改为尾递归形式,可以减少递归调用的栈空间,从而减少程序的内存占用。 下面给出一个优化后的快速排序的 Python 实现: ```python import random def quick_sort(nums): if len(nums) <= 5: return sorted(nums) pivot = median_of_three(nums) left = [] right = [] center = [] for num in nums: if num < pivot: left.append(num) elif num > pivot: right.append(num) else: center.append(num) return quick_sort(left) + center + quick_sort(right) def median_of_three(nums): a = nums[0] b = nums[len(nums) // 2] c = nums[-1] if a < b: if b < c: return b elif a < c: return c else: return a else: if a < c: return a elif b < c: return c else: return b ``` 在上面的代码中,使用了三数取中法选择基准值,并且使用了插入排序来处理长度小于等于 5 的数组。如果数组长度大于 5,就递归地对左右两个子数组进行排序。
写文章

热门文章

  • HTTP 和 HTTPS 的区别(面试常考题) 271198
  • AES加密过程详解 97080
  • 使用海康威视设备在Web端显示实时视频 69896
  • 0-1 背包问题的 4 种解决方法&&算法策略 64281
  • JS字符串分割截取 59640

分类专栏

  • 计算机网络 13篇
  • 分布式论文 17篇
  • 分布式知识 19篇
  • C/C++ 50篇
  • Golang 5篇
  • 操作系统 14篇
  • 算法 9篇
  • 工具使用笔记 1篇
  • 数据结构 6篇
  • 区块链 4篇
  • 项目 11篇
  • 笔试面试 3篇
  • CSDN资讯 2篇
  • 数据库 5篇
  • 学习、生活和感悟 11篇

最新评论

  • 0-1 背包问题的 4 种解决方法&&算法策略

    十一分不解: 动态规划也不对

  • urllib.error.URLError:urlopen error unknown url type:https 解决办法

    躺着听Jay: Setup.dist是什么没找到呀

  • 快速排序的4种优化

    m0_72761900: 我毫不犹豫直接关注表情包

  • 计算机网络:子网划分、子网掩码、CIDR 、路由聚合相关计算详解

    卿月@: C类网不是192.x.x.x以上吗?这里是128呀。

  • 可重入锁(递归锁) & 互斥锁属性设置

    谷延军: 学习了,谢谢

大家在看

  • 化繁为简:中介者模式如何管理复杂对象交互 579
  • DsExcel, GcExcel FOR JAVA 7.2.2 681
  • Scala:如何在IDEA中安装scala插件
  • 历年CSP-J初赛真题解析 | 2024年CSP-J初赛阅读程序(16-32) 122
  • kafka 消息位移提交几种方式:消息重复消息、消息丢失的关键

最新文章

  • TED 演讲原文分享——改变
  • 将孤独视作挑战,倾听内心,自我对话
  • TiDB:基于 Raft 的 HTAP 数据库
2022年15篇
2021年1篇
2020年72篇
2019年36篇
2018年35篇

目录

目录

评论 24
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Tyler_Zx

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或 充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值

玻璃钢生产厂家彩色玻璃钢雕塑多少钱湖南商城艺术玻璃钢雕塑工厂普陀区拉丝玻璃钢雕塑推荐商场美陈模式上海中庭商场美陈研发枣庄景区玻璃钢雕塑安装珠海小区玻璃钢雕塑价格江苏特色商场美陈生产厂家滁州玻璃钢雕塑厂家上海特色玻璃钢雕塑方法大型玻璃钢茶壶雕塑河南艺术商场美陈玻璃钢雕塑 喷漆处理天津玻璃钢景观雕塑施工厂家玻璃钢人物雕塑代理湖南玻璃钢雕塑介绍儿童乐园玻璃钢雕塑哪家有商场美陈宣传片吉安玻璃钢马雕塑玻璃钢雕塑女孩江永玻璃钢造型雕塑昆明市玻璃钢造型雕塑玻璃钢人物雕塑厂商出售西安玻璃钢雕塑行业定价玻璃钢花盆的价格树脂玻璃钢雕塑价格广州发光玻璃钢雕塑延边玻璃钢雕塑公司三明模压法玻璃钢雕塑价格张家口玻璃钢雕塑厂家香港通过《维护国家安全条例》两大学生合买彩票中奖一人不认账让美丽中国“从细节出发”19岁小伙救下5人后溺亡 多方发声单亲妈妈陷入热恋 14岁儿子报警汪小菲曝离婚始末遭遇山火的松茸之乡雅江山火三名扑火人员牺牲系谣言何赛飞追着代拍打萧美琴窜访捷克 外交部回应卫健委通报少年有偿捐血浆16次猝死手机成瘾是影响睡眠质量重要因素高校汽车撞人致3死16伤 司机系学生315晚会后胖东来又人满为患了小米汽车超级工厂正式揭幕中国拥有亿元资产的家庭达13.3万户周杰伦一审败诉网易男孩8年未见母亲被告知被遗忘许家印被限制高消费饲养员用铁锨驱打大熊猫被辞退男子被猫抓伤后确诊“猫抓病”特朗普无法缴纳4.54亿美元罚金倪萍分享减重40斤方法联合利华开始重组张家界的山上“长”满了韩国人?张立群任西安交通大学校长杨倩无缘巴黎奥运“重生之我在北大当嫡校长”黑马情侣提车了专访95后高颜值猪保姆考生莫言也上北大硕士复试名单了网友洛杉矶偶遇贾玲专家建议不必谈骨泥色变沉迷短剧的人就像掉进了杀猪盘奥巴马现身唐宁街 黑色着装引猜测七年后宇文玥被薅头发捞上岸事业单位女子向同事水杯投不明物质凯特王妃现身!外出购物视频曝光河南驻马店通报西平中学跳楼事件王树国卸任西安交大校长 师生送别恒大被罚41.75亿到底怎么缴男子被流浪猫绊倒 投喂者赔24万房客欠租失踪 房东直发愁西双版纳热带植物园回应蜉蝣大爆发钱人豪晒法院裁定实锤抄袭外国人感慨凌晨的中国很安全胖东来员工每周单休无小长假白宫:哈马斯三号人物被杀测试车高速逃费 小米:已补缴老人退休金被冒领16年 金额超20万

玻璃钢生产厂家 XML地图 TXT地图 虚拟主机 SEO 网站制作 网站优化