随机化算法

在随机化算法中,至少使用了一次随机数。该算法的运行时间不只依赖于特定的输入,还取决于生成的随机数。

一个随机化算法的最坏运行时间几乎总是和非随机化算法的最坏运行时间相同。重要的区别在于,好的随机化算法没有不好的输入,而只有坏的随机数。例如,在快速排序中,如果使用第一个元素作为枢纽元,那么对于基本上已排好序的输入来说,运行时间可能会达到最坏的O(N^2)。但如果随机数选择的好,就不会达到这个最坏时间界。这两者的区别在于,对于非随机化算法,总有特定输入能够使它以最坏时间运行,但是随机化算法的时间完全取决于生成的随机数,即使是针对同一输入,两次随机化算法的运行时间也可能不同。

在计算运行时间时,通常假设所有的输入都是等可能的,但在实际情况中并不是这样。所以对于非随机化算法来说,特定的输入可能总是会大量出现,也有可能不会出现。但对于随机化算法来说,特定的输入并不是最重要的,重要的是如何去生成一个好的随机数,只要有好的随机数,时间界就是好的。并且非随机化算法的平均运行时间是对所有可能的输入取平均,而随机化算法是对所有可能产生的随机数取平均,对于快速排序来说,平均时间界是相同的,都为O(NlogN)

随机数发生器:

在随机化算法中,我们需要使用随机数,所以必须有方法去生成它。事实上,真正的随机数在计算机上是不存在的,因我我们一定要有一个算法去获得它,那么所得到的随机数就一定不是真正的随机。一般来说,产生伪随机数就足够了,随机数具有的统计性质,伪随机数也具有其中的大部分。

生成随机数最简单的方法是线性同余发生器,它于1951年由Lehmer首先描述。数x_1,x_2,...的生成满足

x_{i+1}=Ax_i\; mod\; M

为了开始这个序列,必须给出x_0的初值。这个值叫作种子。如果x_0=0那么这个数列显然不是随机的,但如果A,M选择正确,那么其他任何其他1\leq x_0<M都是同等有效的,如果M是素数,那么x_i就绝不可能为0.不同的 A,M可以产生不同的随机数序列,也具有不同的周期。

如果M=11,A=7,x_0=1,那么会生成一个周期为10的随机数序列:

#include <stdio.h>

#define SEED 1
#define A 7
#define M 11

int x = SEED;
int random() {
	return x = A * x % M;
}

int main() {

	for (int i = 0; i < 20; i++) {
		printf("%d ", random());
	}
	printf("\n-------------------------\n");

	return 0;
}

 

如果使A=5,那么序列的周期就只有5:

 

 可以看到A,M的选择对于整个序列的影响是很大的。如果M选择的很大,比如31位的素数,那么就存在适当的A使这个随机数序列的周期非常长,可以满足大部分应用的需求。Lehmer建议使用M=2^{31}-1=2147483647,对于这个素数A=48271是给出整周期发生器的许多值中的一个。对于随机数发生器,贸然的修改可能会造成严重的后果,所以在没有新的成果出来之前,还是使用这一公式来产生随机数。(0,1)区间的小数可以通过除以M来产生。 

如果使用31位素数来产生随机数,最大的问题是过程中可能会出现溢出,虽然这并不是一个错误,但是溢出会改变结果,从而影响伪随机性,有一种改进的算法可以避免这种结果。我们计算M/A的商和余数并分别定义为Q,R,代码如下:

#include <stdio.h>

#define SEED 1
#define A 48271
#define M 2147483647
#define Q (M/A)
#define R (M%A)

int x = SEED;
int random() {
	int ThisSeed = A * (x % Q) - R * (x / Q);
	if (ThisSeed >= 0)
		return x = ThisSeed;
	return x = ThisSeed + M;
}

int main() {

	double aver = 0;
	for (int i = 0; i < 1000; i++) {
		int tmp = 0;
		tmp = random() % 100;
		aver += tmp;
		printf("%d ", tmp);
	}
	printf("%lf\n-------------------------\n", aver / 1000);

	return 0;
}

如果不对溢出进行处理,那么生成的随机数中将会有大量的负数,如果只是简单的将负数结果改为正数,那么随机数生成器的结果也是不理想的:

#include <stdio.h>

#define SEED 1
#define A 48271
#define M 2147483647
#define Q (M/A)
#define R (M%A)

int x = SEED;
int random() {
	return x = A * x % M < 0 ? -A * x % M : A * x % M;//如果是负数,就变为正数
}

int main() {

	double aver = 0;
	for (int i = 0; i < 20000; i++) {
		int tmp = 0;
		tmp = random() % 100;
		if (tmp == 2)
			printf("这里有");
		aver += tmp;
		printf("%d ", tmp);
	}
	printf("\n--------------------------\n%lf\n-------------------------\n", aver / 20000);

	return 0;
}

可以看到即使所生成的随机数已经很多了,在序列中也没有出现2,所以对溢出的处理是非常重要的。C语言库中的rand()函数也是基于线性同余实现的。

如果在公式中添加一个常数可能会得到更好的随机数序列,比如:

x_{i+1}=(48271x_i+1)\; mod\; (2^{31}-1)

最后,随机数发生器有时候是非常脆弱的,比如对于上面的公式,如果Seed=179424105

((48271*179424105)+1)mod(2^{31}-1)=179424105

那么发生器的周期就会变为1. 

跳跃表:

随机化算法的一个应用是以O(logN)时间进行查找和插入的数据结构—跳跃表。

对于一个简单的链表,如果从头部插入,那么花费的时间就是O(1),但查找可能需要遍历链表,所以查找的时间是O(N)

下图表示一种链表,每隔一个节点,就有一个附加的指针指向表中在它之前的第二个节点,这个链表查找一次所花费的时间是O(\frac{N}{2}),最多考察\left \lceil N/2 \right \rceil+1个节点。

如果将上述链表再进行扩展,每隔3个节点,就有一个指针指向表中在它之后第4个位置的节点,这样的链表查找一次花费的时间就是O(\frac{N}{4}),最多考察\left \lceil N/4 \right \rceil+2个节点。

如果一个链表中,每隔2^i个节点,就一个指针指向下一个2^i节点,那么这样的链表,查找一次所需要的时间就是O(logN),并且最多考察\left \lceil logN \right \rceil个节点,在这种数据结构中的查找基本是折半查找。

但上述的插入方式过于呆板,并且不好操作,面对无序的序列很难实现上述的链表,所以我们稍微放松结构条件。将带有k个指针的节点称为k阶节点,任意 k 阶节点上的第 i 阶指针指向的下一个节点至少是 i 阶,这样的数据结构就是跳跃表。在进行插入时,我们可以使用随机数来确定新节点的阶数,并建立相应的节点,在查找操作时,我们先沿着最高阶节点,如果查找值小于最高阶节点的值,那么就从-1阶节点重新开始找,如果大于,则从最高阶节点后的的-1阶节点开始找。

例如,如果我们要查找23,那么首先考察最高阶节点13,我们发现23>13,所以考察13之后的元素,并下降一阶,所以下面考察22,又23>22,所以重复上述操作,考察29,发现23<29,那么我们就需要从22重新开始,并下降一阶考察23,发现就是我们查找的元素。可以看到,在跳跃表中,每一次的考察都能将当前待考察数据的范围缩小一般,所以这是一个折半查找,时间复杂度为O(logN)

在跳跃表中,i 阶节点总共占跳跃表总结点的1/2^i,在使用随机数确定节点阶数时应该参考这个概率。

跳跃表类似于散列表,它们都需要估计表中的元素个数(用于确定最高阶数),如果得不到表中的元素个数,那么我们就需要使用一个大的数来确定表中节点的最高阶数,否则将会使查找效率降低。

跳跃表的实现:

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

//假设有15个数据待插入,那么就需要4阶指
//在这里可以定义四个结构体,表示四种不同阶数的节点,如果只用一种就比较浪费空间
typedef struct jumplist {
	struct jumplist* next1, *next2, *next3, *next4;//next1表示一阶指针
	int val;
}jumplist;

jumplist* CreatList() {
	jumplist* p = (jumplist*)malloc(sizeof(jumplist));
	if (p == NULL) {
		perror("ERROR");
		return NULL;
	}
	p->val = -1;//-1表示头节点
	p->next1 = NULL;
	p->next2 = NULL;
	p->next3 = NULL;
	p->next4 = NULL;
	return p;
}

//查找函数,查找函数的逻辑应该从高阶到低阶
jumplist* find1(jumplist* p, int val) {//在第一阶查找该元素
	jumplist* beforeval = p;//记录查找值的前一个节点
	p = p->next1;//从头节点的下一个节点开始查找
	while (p != NULL) {//如果为空,则说明val不存在,跳出循环
		if (val == p->val)//值相等则返回该指针
			return p;
		else if (val < p->val)//如果查找值小于节点值,那么说明val不存在(根据查找的逻辑),那么beforeval中存放的
							  //就是val要插入位置的前驱,返回beforeval以便val的插入
			return beforeval;
		p = p->next1;//如果查找值较大,就继续向后查找
		beforeval = beforeval->next1;
	}
	return beforeval;//没有找到val则返回val插入位置的前驱
}

jumplist* find2(jumplist* p, int val) {//与三阶的逻辑相同
	jumplist* tmp = p->next2;
	if (tmp == NULL)
		return find1(p, val);
	if (val == tmp->val)
		return tmp;
	else if (val < tmp->val)
		return find1(p, val);
	else return find1(tmp, val);
}

jumplist* find3(jumplist* p, int val) {//考察三阶指针
	jumplist* tmp = p->next3;//头节点三阶指针指向的第一个元素
	if (tmp == NULL)//如果为空,则降低一阶(第二阶)从头节点重新查找
		return find2(p, val);
	if (val == tmp->val)//以下的逻辑都与四阶节点时相同
		return tmp;
	else if (val < tmp->val)
		return find2(p, val);
	else return find2(tmp, val);
}

jumplist* find4(jumplist* p, int val) {//考察四阶节点
	jumplist* tmp = p->next4;//找到跳跃表中的四阶节点
	if (tmp == NULL)//如果不存在,则降低一阶(第三阶)继续查找
		return find3(p, val);
	if (val == tmp->val)//如果四阶节点的值与查找值相等则返回节点地址
		return tmp;
	else if (val < tmp->val)//如果查找值较小,则降低一阶并从头节点重新开始找
		return find3(p, val);
	else return find3(tmp, val);//如果查找值较大,则将该四阶节点视为新的头节点,并降低一阶从它开始查找
}

//插入函数
void insert1(jumplist* p, int val) {//1阶节点的插入
	jumplist* tmp = (jumplist*)malloc(sizeof(jumplist));
	tmp->val = val;
	tmp->next2 = NULL;
	tmp->next3 = NULL;
	tmp->next4 = NULL;
	jumplist* before = find4(p, val);//找到前驱位置
	tmp->next1 = before->next1;
	before->next1 = tmp;
}

void insert2(jumplist* p, int val) {
	jumplist* tmp = (jumplist*)malloc(sizeof(jumplist));
	tmp->val = val;
	tmp->next3 = NULL;
	tmp->next4 = NULL;
	jumplist* before = find4(p, val);
	tmp->next1 = before->next1;
	tmp->next2 = before->next2;
	before->next1 = tmp;
	before->next2 = tmp;
}

void insert3(jumplist* p, int val) {
	jumplist* tmp = (jumplist*)malloc(sizeof(jumplist));
	tmp->val = val;
	tmp->next4 = NULL;
	jumplist* before = find4(p, val);
	tmp->next1 = before->next1;
	tmp->next2 = before->next2;
	tmp->next3 = before->next3;
	before->next1 = tmp;
	before->next2 = tmp;
	before->next3 = tmp;
}

void insert4(jumplist* p, int val) {
	jumplist* tmp = (jumplist*)malloc(sizeof(jumplist));
	tmp->val = val;
	jumplist* before = find4(p, val);
	tmp->next1 = before->next1;
	tmp->next2 = before->next2;
	tmp->next3 = before->next3;
	tmp->next4 = before->next4;
	before->next1 = tmp;
	before->next2 = tmp;
	before->next3 = tmp;
	before->next4 = tmp;
}

void print(jumplist* p) {
	p = p->next1;
	while (p != NULL) {
		printf("%d ", p->val);
		p = p->next1;
	}
	printf("\n---------------------------------------\n");
}

int main() {

	srand((unsigned)time(NULL));
	jumplist* p = CreatList();

	//向表中插入15个数据
	for (int i = 0; i < 15; i++) {
		int option = rand() % 15;
		switch (option) {
		case 14:
			insert4(p, rand() % 100);//4阶节点插入
			break;
		case 13:
		case 12:
			insert3(p, rand() % 100);
			break;
		case 11:
		case 10:
		case 9:
		case 8:
			insert2(p, rand() % 100);
			break;
		default:
			insert1(p, rand() % 100);
			break;
		}
	}

	//打印跳跃表元素
	print(p);

	for (int i = 0; i < 100; i++) {
		int tmp = rand() % 100;
		printf("在跳跃表中找 %d,%s %d\n--------------------------------\n", tmp, tmp == find4(p, tmp)->val ? "找到该元素" :
			"它在表中应该插入在", find4(p, tmp)->val);
	}

	return 0;
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值