自己动手写快速排序

自己动手写快速排序

快速排序算法不算复杂的算法,但是实际写代码的时候却是最容易出错的代码,写的不对就容易死循环或者划分错误。所以还是自己动手多写会印象深刻点。

一、初探—朴素的快速排序算法

void quick_sort(int a[], int l, int u)
{
    if (l >= u) return;
    int m = l;
    for (int i=l+1; i<=u; i++) {
        if (a[i] < a[l])
            swap(a, i, ++m);
    }
    swap(a, l, m);
    quick_sort(a, l, m-1);
    quick_sort(a, m+1, u);
}
这个 朴素的快速排序有个缺陷就是在一些极端情况如所有元素都相等时(或者元素本身有序,如1,2,3,4,5等),朴素算法时间复杂度为O(N^2)。


二、改进—双向划分快速排序算法

一种改进方法就是采用双向划分,使用两个变量i和j,i从左往右扫描,移过小元素,遇到大元素停止;j从右往左扫描,移过大元素,遇到小元素停止。然后测试i和j是否交叉,如果交叉则停止,否则交换i与j对应的元素值。注意,如果数组中有相同的元素,则遇到相同的元素时,我们停止扫描,并交换i和j的元素值。虽然这样交换次数增加了,但是却将所有元素相同的最坏情况由O(N^2)变成了差不多O(NlgN)的情况。比如数组A={2,2,2,2,2}, 则使用朴素快速排序方法,每次都是划分n个元素为1组1个和和1组n-1个,时间复杂度为O(N^2),而使用双向划分后,第一次划分的位置是2,基本可以平衡划分两部分。代码如下:

void quick_sort_2(int a[], int l, int u)
{
    if (l >= u) return;
    int i = l;
    int j = u+1;
    int t = a[l];//选择最左边元素作为枢纽元
    while (1) {
        do {
            i++;
        } while (a[i] < t && i <= u); //注意i<=u这个判断条件,不能越界。
        do {
            j--;
        } while (a[j] > t);
        if (i > j) break;
        swap(a, i, j);
    }
    swap(a, l, j); //注意这里是交换l和j,而不是l和i,因为i与j交叉后,a[i...u]都大于等于枢纽元t,而枢纽元又在最左边,所以不能与i交换。只能与j交换。
    quick_sort_2(a, l, j-1);
    quick_sort_2(a, j+1, u);
}

虽然双向划分解决了所有元素相同的问题,但是对于一个已经排好序的数组如1,2,3,4,5,该方法还是会达到O(N^2)的复杂度。此外,双向划分还要注意的一点是代码中循环的写法,如果写成while(a[i]<t) {i++;}等形式,因为当左右划分的两个值都等于枢纽元时,会导致死循环。


三、继续改进—随机化枢纽元

为了解决上述问题,可以进一步改进,通过 随机选取枢纽元的方式可以在一定程度改进性能。当然,常用的方法还有 三数取中等策略。此外,在数据基本有序的情况下,使用插入排序可以得到很好的性能,而且在排序很小的子数组时,插入排序比快速排序更快。通过使用随机化枢纽元和插入排序来综合改进快速排序,代码如下:

/*快速排序修改版*/
void quick_sort_3(int a[], int l, int u)
{ 
	if (u-l < 7) 
		return insert_sort(a, l, u); //元素数目小于7调用插入排序 
	swap(a, l, randint(l, u)); //随机选取枢纽元 
	int i = l; int j = u+1; 
        int t = a[l];
        while (1) { 
		do { i++; } while (a[i] < t && i <= u); 
		do { j--; } while (a[j] > t); 
		if (i > j) break; 
		swap(a, i, j); 
	} 
	swap(a, l, j); 
	quick_sort_3(a, l, j-1); 
	quick_sort_3(a, j+1, u);
}


/*返回[l,u]范围的随机数*/
int randint(int l, int u)
{ 
	srand(time(NULL)); 
	return rand()%(u-l+1) + l;
}

/*插入排序实现*/
void insert_sort(int a[], int l, int u) 
{ 
	cout << "insert_sort" << endl; 
	for (int i=l; i<u; i++) { 
		for (int j=i+1; j>l && a[j-1]>a[j]; j--) 
			swap(a, j, j-1); 
	}
}

四、再探—采用三数取中方法

除了随机化枢纽元,还可以采用三数取中的方法,三数取中指的就是从数组A[left... right]中选择左中右三个值进行排序,并使用中值作为枢纽元。如数组A={1, 3, 5, 2, 4},则我们对A[0]、A[2]、A[4]进行排序,选择中值A[4]作为枢纽元,数组在三数取中排序后变成A={1,3,4 ,2,5}。我们选择排序后的A[2]=4作为枢纽元,并将其交换到right-1的位置,也就是位置3,从而最终数组变成A={1, 3, 2, 4, 5},然后就可以从i=1和j=2开始双向划分进行快速排序了。

/*三数取中算法*/
int median3(int a[], int left, int right)
{
     int center = (left + right) / 2;
     /*三数排序*/
     if( a[left] > a[center] )
      swap(a, left, center);
     if( a[left] > a[right] )
      swap(a, left, right);
     if( a[center] > a[right] )
      swap(a, center, right);

     /* assert: a[left] <= a[center] <= a[right] */
     swap(a, center, right-1); //交换枢纽元到位置right-1
     return a[right-1]; //返回枢纽元
}

使用三数取中的快速排序如下:

void quick_sort_4(int a[], int l, int u)
{
    if (u - l < 7) return insert_sort(a, l, u);
    int pivot = median3(a, l, u);  //得到枢纽元
    int i = l, j = u - 1;
    while (1) {
        while (a[++i] < pivot)
            ;
        while (a[--j] > pivot)
            ;
        if (i > j)
            break;
        swap(a, i, j);
    }
    swap(a, i, u-1); //注意,这里是i与枢纽元位置u-1交换,因为这里是选择u-1作为枢纽元,与前面选择l作为枢纽元不同
    quick_sort_4(a, l, i-1); 
    quick_sort_4(a, i+1, u);

}

五、号外—非递归写快速排序

非递归写快速排序着实比较少见,不过练练手总是好的。需要用到栈,注意压栈的顺序。这里把划分函数提取出来,便于多次调用,代码如下:

void quick_sort_5(int a[], int l, int u)
{
    if (l >= u) return ;
    stack<int> s; //栈存储划分位置
    int left = l, right = u;
    int p = partition(a, left, right);
    if (p-1 > left) { //左半部分两个边界值入栈
        s.push(p-1); 
        s.push(left);
    }
    if (p+1 < right) { //右半部分两个边界值入栈
        s.push(right);
        s.push(p+1);
    }
    while (!s.empty()) { //栈不为空,则循环划分过程
        left = s.top();
        s.pop();
        right = s.top();
        s.pop();
        p = partition(a, left, right);
        if (p-1 > left) {
            s.push(p-1);
            s.push(left);
        }
        if (p+1 < right) {
            s.push(right);
            s.push(p+1);
        }
    }
}

/*划分函数,返回枢纽元的位置*/
int partition(int a[], int l, int u)
{
    int m = l;
    for (int i=l+1; i<=u; i++) {
        if (a[i] < a[l])
            swap(a, i, ++m);
    }
    swap(a, l, m);
    return m;
}



评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值