1. 记录一下《算法笔记》中生成随机数的思想与方法
  2. 为了引申出随机快排,顺便记录了快速排序算法。

快速排序算法

基本思想:
1. 在数组a中选择一个元素,作为主元,例如选择a[0],保存在temp变量中
2. 用两个指针left和right,分别指向数组的左右两端
3. 当a[right] > temp时right左移,直到a[right] <= temp,然后将a[right]放到a[left]的位置
4. 当a[left] <= temp时left右移,直到a[left] > temp,然后将a[left]放到a[right]的位置
5. 然后重复3和4,直到left==right,将temp放到left位置
6. 对left左边和右边的元素进行上面步骤

以上就是快速排序,很容易看出有递归的思想在,并且已经证明,综合(代码的实现、时间空间复杂度)来看,递归实现比非递归要更优,所以一般选择用递归的方式实现快速排序,下面是递归排序的递归实现。

//对区间[left,right]以a[0]为界进行划分 ,故初始值left=0,right=n-1,下标从0开始 
int partition(int a[], int left, int right){
    int temp = a[left];  //将a[left]存放至临时变量temp 
    while(left < right){ //只要left和right不相遇 
        while(left < right && a[right] > temp) right--; //反复左移right 
        a[left] = a[right];  //将a[right]挪到a[left] 
        while(left < right && a[left] <= temp) left++; //反复右移Left 
        a[right] = a[left]; //将a[left]挪到a[right] 
    }
    a[left] = temp; //将temp放到left与right相遇的地方 
    return left; //返回相遇下标 
} 

//快速排序,初值left=0,right=n-1,下标从0开始 
void quickSort(int a[], int left, int right){
    if(left < right) { //当前区间长度大于1 
        int pos = partition(a,left,right); //将[left,right]按a[left]一分为二 
        quickSort(a,left,pos-1);    //对左子区间递归进行快速排序 
        quickSort(a,pos+1,right);   //将右子区间递归进行快速排序 
    }
}

快速排序的平均时间复杂度是O(nlogn),但是最坏情况的时间复杂度可以到达O(n^2),这是因为待排序列已经基本有序,选择a[left]作为主元无法将序列分成长度相近的两部分,所以时间复杂度就退化到了O(n^2)。

要解决这个问题的一种办法就是随机选择主元,而不是总是用a[left]作为主元,这样虽然算法的最坏时间复杂度还是O(n^2),但对于任意输入数据的期望时间复杂度都能达到O(nlogn),也就是说,不存在一组特定的数据使得这个算法出现最坏情况。(详细证明可以看《算法导论》)

下面就看一下如何生成随机数。

随机数的生成

在C语言中有可以产生随机数的函数,需要添加stdlib.h和time.h头文件。首先在main函数开头加上“srand((unsigned)time(NULL));”,这个语句将生成随机数的种子。然后,在需要生成随机数的地方使用rand()函数。

#include<stdio.h>
#include<stdlib.h>
#include<time.h>

using namespace std;

int main(){

    srand((unsigned)time(NULL));

    for(int i = 0; i < 10; i++){
        printf("%d ",rand());
    }

    return 0;
}

rand()函数只能生成[0,RAND_MAX]范围内的整数(RAND_MAX是stdlib.h中的一个常数,在不同的系统中会有所不同),因此,如果想要输出给定范围[a,b]内的随机数,需要使用rand()%(b-a+1)+a,其中rand()%(b-a+1)可以输出[0,b-a]内的随机数,再+a则可以输出[a,b]内的随机数。

但是,以上的方法仅仅对左右端点相差不超过RAND_MAX的区间的随机数有效,如果需要生成更大的数就不行了。想要生成大范围的随机数有很多种方法,这里记录一种:
1. 先用rand()生成一个[0,RAND_MAX]范围内的随机数
2. 然后用这个随机数除以RAND_MAX,这样就会得到一个[0,1]范围内的浮点数
3. 再用这个浮点数乘以范围长度(b-a+1),在加上a即可
即(int)((double)rand()/RAND_MAX*(b-a+a)+a)

下面的例子是生成10个[10000,60000]范围内的随机数的程序:

#include<stdio.h>
#include<stdlib.h>
#include<time.h>

using namespace std;

int main(){

    srand((unsigned)time(NULL));

    for(int i = 0; i < 10; i++){
        printf("%d ",(int)(1.0*rand()/RAND_MAX*50000+10000));
    }

    return 0;
}

随机快排

上面讨论了快速排序和生成随机数的写法,在此基础上讨论随机快排的写法。

由于需要在a[left……right]中随机选择一个主元,因此不妨生成一个数p,作为主元下标,即a[p]作为主元。然后用a[p]和a[left]交换,按照之前partition()函数的写法即可,代码如下:

int randPartition(int a[], int left, int right){
    //生成left~right之间的随机数p
    int p = round(1.0*rand()/RAND_MAX*(right-left)+left);
    swap(a[p],a[left]); //交换a[p]和a[left] 

    int temp = a[left];  //将a[left]存放至临时变量temp 
    while(left < right){ //只要left和right不相遇 
        while(left < right && a[right] > temp) right--; //反复左移right 
        a[left] = a[right];  //将a[right]挪到a[left] 
        while(left < right && a[left] <= temp) left++; //反复右移Left 
        a[right] = a[left]; //将a[left]挪到a[right] 
    }
    a[left] = temp; //将temp放到left与right相遇的地方 
    return left; //返回相遇下标 
}