Archive for category PROGRAMMING

某道无聊比赛题

又是一周发文时。。。这次写篇槽文好了

题目来自上届的信息技术应用大赛复赛题,题目描述如下:

用1,2,3组成长度为n的字符串,要求相邻子串不能重复。输出所有合法的字符串

和ACM之类的比赛不同,这比赛的题目对于算法居然没有时间要求,估计为了降低参赛门槛吧。

既然没有时间限制,只要想一个正确的算法就行,懒得管他复杂度多奇葩。

最容易想到的一个算法是,每生成一位数字后进行有效性测试,符合的话继续探测。而有效性测试的算法也可以有很多种。

最直接的是从末位往前检测,依次比较相邻的字串是否重复。对于长度为k的串而言,最多比较(指的是重复性)[k/2]次。

这种算法的复杂度应该在O(n^3)。

前天晚上当我洗澡回来时,郁磊刚好给出了这个算法的实现。不过代码木有了- -

而在洗澡时,我恰好想到了一种比较奇葩的解法。此方法和Programming Pearls中求解最大子向量和的递归解法相似。

对于长度为k的字符串,首先分成p1=floor{k/2}和p=2ceiling{k/2}两部分,从中间开始往两边遍历检测。如果检测合法,即说明p1和p2中相邻的字串必定不重复。

接着再同时递归处理两部分,停止条件为最后划分出来的集合只有一个元素。

最初郁磊认为这道题和书中的题目并不类似,这个算法应该不能用。但是我一直认为两个题目的模型类似,算法可行。遂花了大约一刻钟,写出了实现代码。

感谢郭嘉,这算法在不考虑性能的情况下运行良好。

有效性检测的代码如下:

bool CheckStr(wchar_t* sz, int low, int upper)
{
	if (low == upper)
	{
		return true;
	}

	int leftEnd = (low + upper) >> 1;
	int rightStart = leftEnd + 1;

	for (int i = 0; leftEnd - i >= low && rightStart + i <= upper; ++i)
	{
		if (!wcsncmp(&sz[leftEnd-i], &sz[rightStart], i + 1))
		{
			return false;
		}
	}

	bool l = CheckStr(sz, low, leftEnd);
	bool r = CheckStr(sz, rightStart, upper);

	return l && r ? true : false;
}

由于这里的递归为分治策略,所以复杂度分析要稍微复杂些。

我们首先可以确定当串长度为n时整个递归表达式为

T(n) = 2T(n/2) + f(n)

这里的f(n)为中间往两边检测的复杂度,经过分析可以得到

f(n) = sigma(1,[k/2]) = O(n^2)

带入得到递归表达式

T(n) = 2T(n/2) + O(n^2)

根据主定理分析,上述表达式属于第三种情况:n^logb(a)小于f(n)且多项式小于,且存在小于1的正数c使得

af(n/b) <= cf(n)

成立(这里取c=1/4即可)

所以递归式的复杂度是O(n^2),而由于这是生成一个测试位的复杂度,所以整体复杂度为O(n^3)

这里提下生成过程,经过优化的最多测试次数为2(n-1)

虽然无论是直接的反向检测还是递归分治,复杂度都并不好看,但是经过实际测试,发现当n比较大时,符合条件的数反而更少。

另外,以上讨论的方法都是基于生成测试法。似乎此算法的下界也至少是立方复杂度(未证明)。

所以如果要设计更高效的算法的话,应该需要换一个角度。

, , ,

No Comments

C#的下标属性(indexers property)

差不多一周木有写文章了,就拿这个凑个数。。。

前几天看Effective C#(ps:这真是一本好书)时才知道C#支持下标属性,可以实现一些很NB的功能。

下标属性简言之,就是可以用类似数组那样的下标操作来完成对类对象成员的访问。

一个简单的示例代码如下:

	static void Main(string[] args)
	{
		vector vec = new vector();
		vec[3] = 255;
		// will output 510
		Console.Write(vec[3]);
	}

	class vector
	{
		private int[] m_ary = new int[5]{1,2,3,4,5};

		public int this[int index]
		{
			get
			{
				return m_ary[index] * 2;
			}

			set
			{
				m_ary[index] = value;
			}
		}
	}

下标属性没有属性名,且必须指定this。毕竟在使用下标属性时,不需要外的属性名。

另外,属性参数不一定非要是整型,其可以是任何类型。比如使用字符串当作参数,可以很便捷的实现一个字典操作。

并且这个下标属性是可以多维的,指定方式和普通多参数一样用逗号『,』分隔。

由于C#的属性最后都会编译为方法(method),所以个人猜测下标属性最后编译的底层应该是操作符重载,对应C++的overloaded operator[]。不过C#加了一层下标属性,实现起来会更方便而且强大

, ,

2 Comments

堆与优先级队列

优先级队列(Priority Queue)是一种广泛使用的数据结构。其内部元素按照设定的关键字大小(亦可称为优先级别)出队。

使用原始的队列来实现优先级队列,虽然入队效率可以达到Θ(1),但是出队需要对整个队列进行遍历,复杂度需要Θ(N)。

因而优先级队列一般不采用原始队列实现,转而使用堆结构(heap),尤其是最小堆实现。

关于堆数据结构,请参考http://blog.kingsamchen.com/archives/547以及http://en.wikipedia.org/wiki/Heap_%28data_structure%29

基本操作

元素的插入-Insert

和堆排序不同,优先级队列是一个容器,元素需要依次插入。

插入算法比较简单:先将元素插入到下一个可用空间。此时在堆结构中,对应为一个叶子结点。由于元素的插入可能破坏堆特性,即:插入的元素key小于其父结点。此时从结点处向上调整,过程称为PercolateUp,上滤。

PercolateUp操作停止的断言为:{到达根结点 || 已符合堆结构}

另外,如果堆内部数组从下标1开始存放数据,除了使得PARENT/LEFTCHILD/RIGHTCHILD的公式更简洁之外,还可以往首元素填入一个哨兵元素(元素值永远最小)用以简化PercolateUp操作,避免对是否到达根结点进行判断。

Insert操作的复杂度是O(logN),这是最坏情况,但是实际应用中情形往往稍好。

元素获取-ExtractMin

首先,最小堆性质保证了根结点元素具有最大的优先级,所以我们只需要返回根结点的一个copy,并且删除根结点。

但是直接删除根结点会破坏整个堆,而且删起来也蛋疼(想想,的确够蛋疼的)。所以通常会用最后一个叶子的元素去取代根结点,以保持整个堆看起来还存在。

但是这种取代十有八九会破坏堆性质(除非叶子结点优先级和原先根结点相同),所以需要从根结点出向下调整,过程成为PercolateDown,下滤。

PercolateDown基本上都会将元素重新调整到最底层,所以需要临界条件的判断。

另外,PercolateDown和PercolateUp稍有不同:每一次向下一层递进,都一定要确保上浮的元素是几个元素中最小的。而这又会引发另外一个问题,父结点可能有一个孩子,可能有两个孩子。

所以Percolate实现相对较麻烦,而且也难以用哨兵。

ExtractMin的复杂度是O(logN),而且由于下滤的特殊性,一般都会达到这个上界

扩展操作

元素查找-Find

对一种数据结构来说,查找要求是经常会发生的。比如在进程表中查找某个进程。

但是堆不像BinarySearch,并没有表现出太强的顺序性,所以查找通常之能遍历整个堆。复杂度为Θ(N)
Read the rest of this entry »

, , , ,

No Comments

C++中用qsort排序字符串

下午写代码时需要对一个由字符串指针构成的数组进行排序。

为了节约时间和代码量,提高重用性(嗯,你可以看作是我偷懒),决定用crt库函数提供的qsort。

为此,我写了如下的一个比较函数供qsort调用

int cmp(const void* key1, const void* key2)
{
	return wcscmp(reinterpret_cast<wchar_t*> key1,
					reinterpret_cast<wchar_t*>key2);
}

但是调试的结果让我大吃一惊,数组并没有被排序。

排除了其他代码出现问题的可能性之后,我把焦点放在了自己写的cmp函数上。

qsort为了通用性,需要调用一个客户端指定的函数用以比较大小。因为某些类型,主要是结构或者C++中的类(当然,qsort作为C的库函数,不会考虑类)不支持内建的比较。

而为了提高效率,元素的比较是通过指针完成。相应的,这个函数的参数是指向待排序数组元素的指针

字符串数组中的元素是字符指针wchar_t*,而指向这个元素的指针就是wchar_t**。

所以我们需要做的,是将key1和key2都从const void*显式的转换为wchar_t**,因为此时传给函数的参数类型实际上就是这个。

在C中,函数可以如下改写

int cmp(const void* key1, const void* key2)
{
	return wcscmp(*(wchar_t**)key1, *(wchar_t**)key2);
}

如果希望使用C++类型转换风格(C++的类型转换较之要安全但是繁琐),可以如下完成

int cmp(const void* key1, const void* key2)
{
	return wcscmp(*reinterpret_cast<wchar_t**>(const_cast<void*>(key1)),
					*reinterpret_cast<wchar_t**>(const_cast<void*>(key2)));
}

关键的一步是首先要去掉参数的const属性,然后才能使用reinterpret_cast转换为wchar_t**。

直接的参数转换是没法通过编译的

, , ,

1 Comment

表达式树的简单实现

表达式树是树结构的一个经典应用,常用于编译器的设计。

在表达式树中,叶子通常是常数值或者变量名,统称为操作数(operands)。而其他非叶子结点则包含各种操作符(operators)。

在不同的词法解析中,表达式树的分支设计也不同。对于简单的诸如表达式求解而言,表达式树往往采用二叉树。这是因为每一个操作符正好对应两个操作数。

而在包含一元操作符(e.g自增自减操作符)或三元操作符的复杂情况下,每个操作符结点拥有的不再是两个孩子结点,此时二叉树便显得不太适用。

在此处的简单实现,我们只考虑适合二叉树的表达式求解,其余情况请自行翻阅编译原理相关书籍。当然,无论是哪种情况,本质的算法思想是相同的。

0.构造表达式树

要构造一个表达式树,首先要将一般的中缀表达式(infix)转换成逆波兰式(即后缀表达式,postfix)。

PS:关于逆波兰式以及infix和postfix的转换,请参考我之前的一篇文章http://blog.kingsamchen.com/archives/637

对于中缀表达式(a+b)*(c*(d+e)),其逆波兰式为ab+cde+**,我们现在结合这个表达式,阐述利用逆波兰式构造表达式树的一般步骤。

在构建表达式树时,我们需要一个堆栈,用于保存树或者结点的指针,记这个堆栈为S。

我们对逆波兰式进行遍历,如果碰到的是

(1)操作数,那么则建立一个叶子结点,数据为操作数,并将结点指针保存到堆栈中
(2)操作符,那么从堆栈中弹出两个指针,分别为p2和p1.建立一个结点,内容为操作符,然后分别将结点的左右孩子设置为p1和p2,最后将结点指针压入栈

一直重复以上步骤即可。最后堆栈中保存的即是表达式树的根结点指针。
Read the rest of this entry »

, , , , ,

No Comments

求给定数组子数组中最接近0的和

此题来源于经典的求给定数组子数组最大和的变形,同时也是Programming Pearls的课后习题。

不过书中对于此题的分析存在瑕疵,正确的算法需要在原算法基础上做一点小小的改动。

此题的描述如下:

设计一个算法,对给定一个有正有负的整数数组,求数组的子数组中最接近0的和

解决过子数组最大和这一问题,很容易知道,蛮力的枚举不是一个好方法,因为这会使得时间复杂度提高到难以接受的地步。

类似的,我们可以尝试使用“将x[0...n-1]扩展为x[0...n]”的思想,建立一个累积和表cumSum进行处理。这里假设输入数组为x[n],那么则有

由此我们发现,cumSum两个元素的差值恰好对应一个子数组的和。

剩下我们只需要对cumSum进行排序,然后寻找最接近(差值的绝对值最小)的相邻的两个数即可。
Read the rest of this entry »

, ,

3 Comments

逆置无符号整数二进制位

前两天看Pointers On C时碰到的问题,问题的详细描述如下:

编写一个函数,该函数接受一个无符号的整数(unsigned int),将这个整数在二进制上进行逆置(即逆置bit位)。

如,十进制的整数7的二进制位是 0000 0000 0000 0111,逆置后的结果为 1110 0000 0000 0000,这个数是3758096384。

函数应当返回逆置后的数。

需要注意的是,为了保持可移植性,并不假设int类型具有32bit长,所以编写的函数要能同时对16bit和32bit有效。

解决问题的关键是,如何正确处理不同字节长。

实际上,我们并不一定需要知道具体的字节长,我们只需要知道何时操作应该停止即可。

联想bitwise相关内容,这里使用一个unsigned int的标志变量i,且将其初始化为1,并对变量i执行单位左移操作。

当i的值为0时,表示左移出界,此时操作应该停止。

demo如下:

typedef unsigned int uint;

// reverses value on bit based
uint ReverseBit(uint value)
{
    uint answer = 0U;

    for (uint i = 1U; i != 0; i <<= 1)
    {
        answer <<= 1;

        if (value & 1)
        {
            answer |= 1;
        }

        value >>= 1;
    }

    return answer;
}

我们每次检测value的第一位(最低位),根据结果设置answer的相应位。

每次操作前,answer都需要右移一位,为下一次操作留出空间。这么做有两个原因:

其一,右移后,之前的bit值会上升到高位,正好迎合我们逆置的目标

其二,每次操作后,answer不能进行位移操作,否则最后一次操作之后的位移会使得代码运行的不正确。

所以我们需要在操作前进行单位位移

, ,

No Comments

两道小题

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

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

CRegistry V2

从7月6日开始重构CRegistry,一直到昨天通过测试正式结束,小小的感慨下~~

期间碰到实习大作业展示和考试,以及Dave造访、回家等琐事,deadline被一拖再拖。还好,现在总算结束了。

CRegistry第一版来自个人写ARV(AutorunLoadViewer)时的封装。由于水平和时间的原因(V1是在高二时写的…),当时设计的CRegistry不是很合理,存在一些比较大的缺点。

于是便想到重构CRegistry,使其变得更具通用性和易用性。

经过重构的CRegistry以下优点:

完美支持不同编译器的Win32项目(最初只支持VC的MFC)

更强大的操作支持

重新设计的函数接口,更加符合面向对象

新版CRegistry的头文件如下:

/**************************************************

** Project:CRegistry Design

** File:Registry.h

** Edition:v2.0.0 Demo

** Coder:KingsamChen [Code Think]

** Last Modify:2011-7-15

**************************************************/

#if _MSC_VER > 1000
#pragma once
#endif

#ifndef _CREGISTRY_38037F33_2602_44f7_8C0F_EBB82B1408F3
#define _CREGISTRY_38037F33_2602_44f7_8C0F_EBB82B1408F3

#include <Windows.h>
#include <cstdio>

class CRegistry
{
	public:
		CRegistry();
		CRegistry(HKEY key);
		CRegistry(const CRegistry& reg);
		~CRegistry();

	public:
		CRegistry& operator=(const CRegistry& reg);
		inline void Key(HKEY key);
		inline HKEY Key() const;
		inline DWORD GetErrorCode() const;

	public:
		BOOL Open(HKEY keyParent, LPCTSTR pszKeyName, REGSAM samDesired = KEY_READ | KEY_WRITE);
		inline void Close();
		BOOL Create(HKEY keyParent, LPCTSTR pszKeyName,
					DWORD keyOptions = REG_OPTION_NON_VOLATILE,
					REGSAM samDesired = KEY_READ | KEY_WRITE,
					LPSECURITY_ATTRIBUTES pSecurityAttributes = NULL);
		BOOL IsKeyExist(HKEY keyParent, LPCTSTR pszKeyName);
		BOOL DeleteKey(LPCTSTR pszSubKey);
		BOOL DeleteTree(LPCTSTR pszSubKey);
		BOOL DeleteValue(LPCTSTR pszValueName);
		BOOL EnumKey(DWORD index, LPTSTR szKeyName, LPDWORD pNameLength);
		BOOL EnumValue(DWORD index, LPTSTR szValueName, LPDWORD pNameLength,
						LPDWORD pDataType = NULL,
						LPBYTE pData = NULL, LPDWORD pDataSize = NULL);
		BOOL QueryBinaryValue(LPCTSTR pszValueName, LPVOID pData, LPDWORD pDataSize);
		BOOL QueryDwordValue(LPCTSTR pszValueName, DWORD& data);
		BOOL QueryStringValue(LPCTSTR pszValueName, LPTSTR szData, LPDWORD pLength);
		BOOL SetBinaryValue(LPCTSTR pszValueName, LPCVOID pData, DWORD dataSize);
		BOOL SetDwordValue(LPCTSTR pszValueName, DWORD data);
		BOOL SetStringValue(LPCTSTR pszValueName, LPCTSTR pszData, DWORD type = REG_SZ);

	public:
		enum{OPERATION_ERROR = -1, NAME_MAX = 255};		

	private:
		HKEY m_hKey;
		DWORD m_errorCode;
};

inline void CRegistry::Key(HKEY key)
{
	if (key != NULL)
	{
		m_hKey = key;
	}
}

inline HKEY CRegistry::Key() const
{
	return m_hKey;
}

/*
	Description:
		Closes a handle to the specified registry key
	Parameters:
		none
	Return Value:
		none
*/
inline void CRegistry::Close()
{
	if (m_hKey != NULL)
	{
		::RegCloseKey(m_hKey);
		m_hKey = NULL;
		m_errorCode = 0L;
	}
}

/*
	Description:
		Returns last-error code value. The code value is maintained on a
		per-object basis. Note that last-error code saved in object will be
		updated after any operations that are revelant to registry.
	Parameters:
		none
	Return Value:
		The return value is last-error code.
*/
inline DWORD CRegistry::GetErrorCode() const
{
	return m_errorCode;
}

#endif

需要注意的是,考虑到实际的操作情况,对于注册表Value的Query/Set暂时只有最常用的三种数据类型。如果你需要对其他数据类型进行操作,请自行添加。

本项目遵从Apache License,请合理使用源代码。

Download CRegistry V2 SRC

, , , , ,

2 Comments