Posts Tagged 算法

两道小题

题目虽小,却可以说明很多东西

0.在二分搜索中返回元素第一次出现的位置

对于传统的二分搜索,如果要搜索的元素存在重复,那么其只能返回这个元素任意一个位置。

如果我们需要返回这个元素第一次出现的位置,且希望算法仍然保持Θ(logn)的复杂度,那么应该如何设计算法?

我们可以适当的引入一个保持循环不变式成立的断言条件,来作为整个算法思路的突破口。

对于原始的二分搜索来说,这个断言是mustin(l, u),因为每次迭代我们都需要保证目标元素在一个区间之内(假设元素存在)。

相似的,我们可以对这个修改版的二分搜索设立断言条件:a[l]<t<=a[u]。

这个断言条件对于整个算法来说都是至关重要的,以至于只要每次迭代都能使如上断言满足,那么迭代结束后,元素第一次出现的位置就是u。(仍然假设元素存在)

考虑到输入数据的极端情况,l和u初始值应该分别设为-1和n。其中n为元素个数,这里为zero-based。

除此之外,我们只需要对于不存在的情况稍加处理即可。

该算法实现如下:

typedef int DataType;
int BinSearchEx(int data, const DataType a[], int count)
{
	int lower = -1, upper = count;
	int pos;

	// a[l]<t<=a[u]
	while (lower + 1 != upper)
	{
		int mid = lower + ((upper - lower) >> 1);
		if (a[mid] < data)
		{
			lower = mid;
		}
		else
		{
			upper = mid;
		}
	}

	pos = upper;
	if (pos >= count || a[pos] != data)
	{
		pos = -1;
	}

	return pos;
}

一个有意思的结果是,这个扩展版的二分搜索的效率相对要比原始的算法要高,因为每次迭代中元素比较的次数降低了。

扩展问题:如果我们需要返回元素最后一次出现的位置,又该如何设计算法?

A:将断言条件改为a[l]<=t<a[u]

1.乘方的递归算法

如果要计算一个数的n次方,朴素的迭代算法需要Θ(n)的复杂度。这对于日常计算来说已经完全够用,毕竟目前内建整型的数据最多也只能表示到264-1。

但是对于大整数计算来说,线性复杂度并不太让人满意。

而如果利用递归算法,则可以将复杂度降低至Θ(logn),这可以说是一个质的飞跃。

递归算法的正确性在于:计算an的结果可以转化为an/2的平方,而an/2又可以再经过一次转换,再继续递归求解(以上假设n为偶数)。

此递归表达式为:T(n) = T(n/2) + Θ(1)

该算法实现如下:

int Pow(int num, int x)
{
	assert(x >= 0);

	if (0 == x)
	{
		return 1;
	}
	else if (1 == x)
	{
		return num;
	}

	// odd
	if (x & 1)
	{
		return num * Pow(num * num, x >> 1);
	}
	else
	{
		return Pow(num * num, x >> 1);
	}
}

需要注意的一点是,利用Pow(num, x/2) * Pow(num, x/2)来计算偶次方是错误的,因为这样会使得复杂度骤升至线性。

, , , ,

2 Comments

日期计算问题

在实际应用中,经常会碰到需要计算和日期有关的问题。其中最常见包括:计算两个给定日期间的天数,返回给定日期的星期以及生成指定年月的日历等。

这些问题实际上都不是非常难(但是某些极易出错),但是运用某些trick可以使得编码更加简洁,算法效率更高。

Q1:如何计算两个给定日期间的天数

首先我们需要明确所谓两个日期间的天数这一定义。

从习惯上说,两个日期间的天数一般看作“从日期一到日期二所需要的天数”。即2011/7/20和2011/7/21之间存在一天。

这个问题在网上流传着众多算法和详细的代码实现,但是大部分都显得比较累赘且低效。比如我在Google的结果中找到的一个sampl(http://sillydong.com/myc/day-counter.html),写了80多行代码(减去了结构声明和头部注释)。

ps:给出URL仅仅用于说明问题,绝无任何恶意。

计算这个问题真的需要那么复杂吗?其实,很多时候,问题都是被自己搞复杂的~

我们首先需要明确一点,要正确的计算两个给定日期间的天数,不可避免的要解决如下两个问题:(1)跨年分的日期 (2)闰年的2月份。

事实上,我们可以根据一个更高效的算法来处理这个问题:

(1)假设没有闰年,即一年都是365天,分别返回两个给定日期在当年日期中的编号(对应的第x天,需要正确处理闰年),分别设为d1和d2

(2)另t为d2减去d1的差。如果两个日期在同一年,则直接返回t。否则加上两个年份之差的365倍

(3)然后在从较小小的日期年份开始遍历,碰到闰年,将天数自增1.但是不用考虑较大日期年份是不是闰年。

这个算法并不复杂,很容易实现。但是要达到简化编码的目的,每一步我们都需要使用一些些trick。
Read the rest of this entry »

, , ,

14 Comments

逆波兰式与表达式求解

一、历史

逆波兰式表示法(Reverse Polish Notation, RPN)可以看作波兰表示法(PN)的一种逆向表示。后者最初由波兰数学家Jan Lukasiewicz在1920年提出,由此得名。

在波兰表示法中,每个操作符均在相应的操作数之前,这也是牛逼的Lisp采用的表达式风格。与此相对的,逆波兰式中,每个操作符都在其操作数之后,由此也得名“后缀表示法(Postfix)”

ps:还记得C/C++中的 op += opr么?

例如表达式(3-4)*5的逆波兰式为:3 4 – 5 *

而表达式3-4*5的逆波兰式为:3 4 5 * -

由此可以发现,逆波兰式中无需用括号来指定优先级,这是RPN一个非常有用的特点。

基于此特点,逆波兰式被提出用于解决表达式求值的问题。

ps:关于逆波兰式的更详细信息,请参看http://en.wikipedia.org/wiki/Reverse_Polish_notation

二、表达式预处理

应用RPN求解表达式的难点在于,如何将一个标准的表达式转换成RPN,即Infix to Postfix

对于词法分析器来说,生成RPN可以通过构造一个词法树,然后对其后序遍历。

但是对于简单的应用来说,使用堆栈就可以解决这个问题。

首先我们需要一个堆栈用于保存操作符,假设为s,那么相应算法描述如下:

1.碰到操作数,直接输出
2.碰到操作符op
如果op是( 则直接入栈s
如果op是) 那么s弹栈并输出,一直到弹出(
如果op是+-*/,那么
如果s为空 或者 top[s] == ( 或者 op的优先级高于top[s] 则 op入栈
否则 s弹栈并输出,接着重复上一步
如果表达式分析完毕后栈s不为空,将剩下的操作符全部输出

如此,Infix已经被转换成了Postfix

这里需要注意的是,为了能正确的应用此算法,一般将()的优先级设置为最高

Read the rest of this entry »

, , ,

1 Comment

二分查找缺失整数和一维向量旋转问题

首先,这两个问题都来自Programming Pearls(编程珠玑),且都具有令人想不到的解法和应用,值得细细分析和思考。

一、二分搜索缺失整数

此问题描述如下:

给定一个最多包含40亿个随机排列的32位整数文件,找出一个不在文件中的32位整数。在具有足够内存的的情况下,如何解决该问题?如果有几个外部临时文件可用,但是仅有几百字节的内存,又该如何解决?

如果内存足够,那么该问题就变得非常简单。直接利用位向量为每一个数设置标志位,然后遍历搜索即可。

相关的方法在第一章已经使用过,在此不再进行阐述。

而对于第二种情况,问题就变得不是那么容易解决。

由于受到内存大小的限制,位向量的做法显然不可取,而且,提供的临时文件又该怎么使用?

这里有一个高效的解法可以只需要少数的内存(满足题设)和几个外部文件。并且,它还和二分搜索的思想有关。

该算法的描述如下:

以二进制形式来看待每一个整数,第一趟对每一个整数的第一位进行检测,如果第一位是0,则写入文件A,否则写入文件B。

第一趟完毕后,任意一个文件中整数个数不会超过20亿。根据个数较少的文件(如果一样多的话,任选一个文件),把缺失整数变量的第一个二进制设置为对应数值,再把此文件设置为输入文件,重复此操作,logn次之后确定缺失整数。

现在对该算法进行分析:
Read the rest of this entry »

, , , ,

No Comments

表达式求解的一个简单实现

RT

好久没写Blog,写一篇凑凑数。

来自某个很无聊的“作业”,最初是让写一个简易科学计算器,为了避免做UI的麻烦,直接选择表达式求解的方式实现。

算法原型来自Lieo,经过部分改进后,支持负数和多重括号。

因为只是expression-evaluate的一个demo,所以没有写得太详细。

不支持小数,没有异常处理,包括除数为零。

这里说下算法:

首先需要有两个堆栈或者支持FILO性质的线性表(我用的是vector,便于调试时观察内部元素是否正确),分别保存操作数和操作符。

负数通过额外的符号位实现,并规定,操作符后面跟着的“-”表示负号,符号位逻辑求反。

遍历表达式时,如果碰到操作数,直接入操作数栈。碰到操作符op,先和操作符栈顶的元素比较优先级,如果优先级大于栈顶元素,入栈(如果操作符栈为空,直接入栈),否则取出操作符栈顶元素和操作数进行计算,一直到操作符栈空或者栈顶元素优先级小于操作符op。

对于括号特殊处理:

碰到“(”,直接入栈,碰到“)”,取出操作符栈和操作数栈计算,直到操作符栈中弹出“(”。

为了简化算法,表达式尾部需要有#符号充当哨兵元素,并规定#符号的优先级最小。

提供核心代码,全部代码请翻看SRC

#define pop(x) x.back(); \
				x.pop_back();

struct NumData
{
	NumData() : signal(false), pos(0)
	{
		wmemset(sz, L'\0', NUM_LEN);
	}

	enum{NUM_LEN = 10};
	wchar_t sz[NUM_LEN];
	bool signal;
	unsigned int pos;
};

int EvaluateExpression(wchar_t* pszExpression)
{
	stackOperand oprand;
	stackOperator oprator;
	bool isNumbericMode = true;
	NumData data;

	for (wchar_t* p = pszExpression; *p != NULL; ++p)
	{
		if (*p >= L'0' && *p <= L'9')
		{
			data.sz[data.pos++] = *p;
			isNumbericMode = false;
		}
		else if (L'-' == *p && isNumbericMode)
		{
			data.signal = !data.signal;
		}
		else	// pure operators
		{
			isNumbericMode = true;

			if (L'(' == *p)
			{
				oprator.push_back(*p);
			}
			else if (L')' == *p)
			{
				int num;

				if (data.pos)
				{
					num = FormatNumtoInt(data);
				}
				else
				{
					num = pop(oprand);
				}

				do
				{
					wchar_t op = pop(oprator);

					if (L'(' == op)
					{
						oprand.push_back(num);
						break;
					}
					else
					{
						int num2 = pop(oprand);
						num = Calculate(num2, num, op);
					}

				} while (true);
			}
			else	// operators except ( and )
			{
				int num;

				if (data.pos)
				{
					num = FormatNumtoInt(data);
				}
				else
				{
					num = pop(oprand);
				}

				do
				{
					if (oprator.empty() || (GetPriority(*p) > GetPriority(oprator.back())))
					{
						oprand.push_back(num);
						oprator.push_back(*p);

						break;
					}
					else
					{
						int num2 = pop(oprand);
						wchar_t op = pop(oprator);

						num = Calculate(num2, num, op);
					}

				} while (true);
			}

			//DumpStack(oprand);
		}
	}

	return oprand.back();
}

int FormatNumtoInt(NumData& data)
{
	assert(data.pos);

	int num = _wtoi(data.sz);
	num = data.signal ? -num : num;

	wmemset(data.sz, L'\0', NumData::NUM_LEN);
	data.pos = 0;
	data.signal = false;

	return num;
}

Download EvaluateExpression SRC

, , , ,

1 Comment

函数增长和基本算法复杂度分析

在设计和分析算法时,我们关心的不是算法对于某一特定数据量的性能,而是算法随着数据规模的增长所表现出的性能。

所以当我们描述一种算法比另一种高效,并不是指这种算法(在某个数据量下)比另一种运行的快,虽然有时候这种描述符合事实,而是指这种算法在数据增长时表现得比另一种算法更加优异。

不经意间,我们提到了增长。

一、函数的增长

事实上,几乎所有的算法增长都可以转化为函数或表达式的增长关系分析。不过要注意的是,此时的函数增长关系有别于传统中的数学意义。

在算法分析中,存在如下几个重要的定义:





定义1和2分别给出了函数的渐进上界和渐进下界,与之相对的4和5给出的则是非渐进紧确的渐进上界和渐进下界。而定义3给出了函数的渐进确界。

非渐进紧确上下界的意义在于明确表示函数不会达到上下界一样的数级。

在很久之前,包括现在,人们习惯于使用O来表示算法一般情况下的复杂度和最坏情况下的复杂度。而这种表示法存在一个很大的问题:容易混淆函数的渐进上界和渐进确界。

例如:n = O(n),同时n = O(n^2),但是前者就比后者更加紧确

为了解决这个问题,有一个人在1970年提出用Θ表示渐进确界,O只表示渐进上界。4年后,这个人获得了图灵奖,成为计算机科学领域的代表人物之一,他叫D.E Knuth
Read the rest of this entry »

, , , ,

1 Comment

非常规排序问题

让问题飞一会儿1

考虑如下问题:

描述:假设有一个包含最多n个正整数的文件,每个数均小于n,此处n=10^7.且每个数字至多只出现一次。

目标:设计一个算法,将文件中的正整数按升序排列输出。

条件:RAM大约在1MB,最大占用不超过1.5MB2,运行时间在10S内,无磁盘空间约束。

这个问题抽象自某个数据库系统对美国800免费电话的排序问题:

当时美国免费电话由区号800加7位数字构成。系统在某些时候需要对这些号码进行排序。

对这个问题认真思考后,我们来考虑一些由这个原问题得到的扩展问题。

 

扩展问题

Q1:为了解决原问题,需要相应测试数据。我们需要一个算法,该算法能够产生[0,n)间不重复的含有K个(0<K<=n)元素的整数序列.

Q2:原问题中,RAM最大的空间是1.5MB,若提供的空间严格的只有1MB,对应的算法应该怎样修改?

Q3:如果数字允许最多重复出现10次,算法又该如何改进?

Q4:现在美国免费电话的区号有:800、877和888。而且还有不断增加的趋势,如何用1MB内存对上述号码进行排序?

Q5:在Q4的条件下(不包含1MB内存约束条件),如何将这些号码保存在一个集合中,实现对给定号码的快速查找?

Q6:在大空间中,初始化需要消耗大量时间。设计算法避免初始化。新算法的访问时间复杂度应该是O(1),且额外空间大小正比于排序数据大小。

 

原题分析

1.如果不考虑内存约束条件,即可以将10^7个整数全部读到内存中,在进行排序(比如QuickSort)。但是这个需要10^7 / 1024 / 1024 × 4 = 38MB。这个和允许的内存空间差距太大了。(感谢LittleHorse的debug :-)

2.如果采用基于磁盘文件的归并排序,则可以有效降低内存占用。

我们设一次排序K个数,那么有 K * 4 / 20^20 = 1.5 => K = 393216

我们可以一次读入35W个数据进行排序(内存中可以用QuickSort),得到输出文件后再利用MergeSort进行合并。

但是很不幸的,我们需要N = 10^7 / K = 29次文件I/O。

是否有算法既能降低内存要求又能避免大量的磁盘I/O?

3.回忆线性排序中的计数排序(CountSort),结合实际情况,我们考虑采用位向量(bitset)来表示数据。

bitset表示法即:假设v是一个位向量,用于表示非负整数。那么若第K个数存在,则v[k]为true,否则为false。

易知,1000W个数需要1000W bits = 1.2MB3,内存符合要求。
Read the rest of this entry »

, , , , ,

6 Comments

二叉查找树及一个封装拙劣的BST类

二叉查找树(Binary Search Tree, BST)是一种特殊的二叉树,常常用来作为进行数据查找的存储结构。

对于长度为n的线性表来说,搜索一个数据的时间是O(n),而对于BST,搜索的运行时间复杂度和高度成正比。

由二叉树的性质可以知道,当二叉树比较平衡时(比如完全二叉树),其高度h近似等于logn,n为结点数。所以,较为平衡的BST的搜索时间复杂度可以达到O(logn)。

 

1. 二叉查找树(BST)

BST通常用链表表示,每个结点至少会包含:parent,left,right和key等附加的数据域。如果孩子结点或者父结点不存在,则用NULL表示。

易知,树根结点是唯一一个父结点为NULL的结点。

BST有一个如下所述的重要存储性质:

在一个BST中,对任何结点x,其左子树中的key不大于x的key,其右子树中的key不小于x的key。即

如图为一棵BST

根据BST的性质可以知道,如果对BST进行中序遍历(Inorder Traverse),可以将BST中所有元素按有序输出

遍历一棵含有n个结点的BST所需要的时间复杂度是O(n),因为每个结点都会被访问。

 

2. BST类的设计

可以利用C++将BST封装成一个类,便于以后调用。且为了使得这个类通用,我们将类写成模板类

我们需要一个Node结构和一个控制结构的类CBinSearch。

为了尽可能地遵循封装思想,我们将Node类写在CBinSearch类的protected成员中,这样Node结构对外界是透明的。

考虑到某些函数要求客户传入一个指向Node结点的指针,我们仿照STL的iterator,定义出两个公共的结点指针类型

typedef  Node* pos;
typedef const Node* const_pos;

所以,最后我们的类声明大概是这样的
Read the rest of this entry »

, , , , , ,

8 Comments

线性同余方程和中国剩余定理

此篇是对http://blog.kingsamchen.com/archives/581中韩信点兵问题的一个详细的补充说明。

在分析这个问题前,有必要知道整个来龙去脉

一、同余

同余表示两个整数除以正整数m时有相同余数的一种情况。

定义:a和b为整数且m是正整数,如果有m|(a-b),即m是a-b的因子,则称a、b模m同余,记a≡b(mod m)

然后介绍几个重要定理:




Read the rest of this entry »

, , , , ,

No Comments

Algorithms Exercise 1

回家前在图书馆碰到了刘汝佳的《算法竞赛入门经典》,于是便借回来随手翻翻

个人实在不太喜欢这类风格的书,但是觉得某些题目不错,就随手做做,权当练习

1、位数

描述:输入一个不超过10^9的正整数(由输入者负责保证),输出它的位数。例如12735的位数是5.不要使用任何数学函数,只使用四则运算。

思路1:设接受的的n位整数为N,让i从1取到9(事实上,只要不发生溢出,可以取到上限),并令 k = N / 10^i,当k == 0时,则所求位数为i。算法复杂度为O(n)

int QueryDigit(int nNum)
{
    int nDigit, nDiv;

    for (nDigit = 1, nDiv = 10; nNum / nDiv; ++nDigit)
    {
        nDiv *= 10;
    }

    return nDigit;
}

思路2:查表法。很容易想到,每个数所在的位数区间都是确定的。比如12735在10000~99999这个区间范围内。而这个区间范围内的数都是5位。查表法需要写较多的代码,但是运行起来速度会快得多,不过只有在要查询的数字比较多的情况下才能体现。易知,复杂度为O(1)

int QueryDigit(int nNum)
{
    int DigitTable[] = {1, 2, 3, 4, 5, 6, 7, 8, 9};
    int nDigit;

    // a long long if statement
    if (0 <= nNum && nNum < 10)
    {
        nDigit = DigitTable[0];
    }
    else if (10 <= nNum && nNum < 100)
    {
        nDigit = DigitTable[1];
    }
    else if (100 <= nNum && nNum < 1000)
    {
        nDigit = DigitTable[2];
    }
    else if (1000 <= nNum && nNum < 10000)
    {
        nDigit = DigitTable[3];
    }
    else if (10000 <= nNum && nNum < 100000)
    {
        nDigit = DigitTable[4];
    }
    else if (100000 <= nNum && nNum < 1000000)
    {
        nDigit = DigitTable[5];
    }
    else if (1000000 <= nNum && nNum < 10000000)
    {
        nDigit = DigitTable[6];
    }
    else if (10000000 <= nNum && nNum < 100000000)
    {
        nDigit = DigitTable[7];
    }
    else
    {
        nDigit = DigitTable[8];
    }

    return nDigit;
}

2、韩信点兵

描述:相传韩信才智过人,从不直接清点自己军队的人数,只要让士兵先后以三人一排、五人一排、七人一排地变换队形,而他每次只要掠一眼排尾的人数就知道总人数。输入3个非负整数a,b,c,表示每种队形排尾人数(a<3,b<5,c<7),输出总人数的最小值。

思路1:题目实质是求x=3*k1+a = 5*k2 + b = 7*k3 + c。可以通过令x从1开始进行尝试,第一个符合的数就是所求的数。分析可知,算法的复杂度是O(n),这里的n只与所求数有关。

但事实上,存在复杂度为O(1)的算法。因为此题可以通过公式计算得到。

思路2:这道题在数论上颇有名气,其本质是求线性同余方程组的解。解法最早由宋朝秦九韶提出,并被国外数学家冠以“中国剩余定理”

ps:这本号称入门级的算竞赛辅导书(似乎还是面向中学生)居然会出现线性同余方程组求解的问题,表示这让我很诧异。

关于“中国剩余定理”的详细说明和分析将在未来几天内(起码得等我到家)完成。

由“中国剩余定理”可以得到韩信点兵问题的通解公式

知道了公式,代码就显得极其简单
Read the rest of this entry »

, , , ,

9 Comments