Archive for category PROGRAMMING

NULL和nullptr

0.NULL的前世今生

对于C和C++程序员来说,一定不会对NULL感到陌生。但是C和C++中的NULL却不等价(别惊讶,这是真的)。

NULL表示指针不指向任何对象,但是问题在于,NULL不是关键字,而只是一个宏定义(macro)。

在C中,习惯将NULL定义为void*指针值0:

#define NULL (void*)0

但同时,也允许将NULL定义为整常数0

An integer constant expression with the value 0, or such an expression cast to type void *, is called a null pointer constant.55) If a null pointer constant is converted to a pointer type, the resulting pointer, called a null pointer, is guaranteed to compare unequal to a pointer to any object or function.

在C++中,NULL却被明确定义为整常数0:

#define NULL 0

A null pointer constant is an integral constant expression (5.19) rvalue of integer type that evaluates to zero.

导致不同定义的根本原因是:隐式类型转换(implicit conversion)。

C的定义使得存在void*到0的隐式类型转换,而宏观表现则是,void*指针可以直接赋值给其他的指针类型

int* ptr = 0;
int* pptr = (void*)0; // both are legal in c

但是C++却不支持void*到其他类型的隐式转换,所以上面的代码在C++中并不能全部通过编译。

1.C++的解释

为什么C++在NULL上选择不完全兼容C?原因和C++的重载函数有关。

C++通过搜索匹配参数的机制,试图找到最佳匹配(best-match)的函数,而如果继续支持void*的隐式类型转换,则会带来语义二义性(syntax ambiguous)的问题。

考虑下面两个重载函数:

void foo(int i);
void foo(char* p)

foo(NULL); // which is called?

编译器同时找到了两个最佳匹配,以至于连编译器都不知道选择哪个。

为了消除混乱,C++才会将NULL定义为整常数0。

但是,这么做又带来了另一个问题:NULL和整型发生了重叠。

2.C++的选择

为了彻底解决NULL带来的问题,C++0x标准引入了一个新的关键字:nullptr。

nullptr是一个很神奇的关键字,他被定义为std::nullptr_t类型,这是一个泛指针类型(这个术语是我自己加的-.-),支持到其他指针类型的自动转换。并且不支持到除了bool类型以外的整型的转换。

由于nullptr具有类型,且支持到任何兼容的指针类型的转换,所以对于上述重载函数的调用,编译器会很明确的选择第二个函数。

不过,引入新关键字并不代表解决了一切问题。其代价也很明显:向后兼容性/习惯性和编译器的支持程度。

C++0x中保留了原来的NULL,所以旧的代码也可以通过编译。并且为了和C保持最大程度的兼容,在一段时间内,NULL都应该是支持的。

至于程序员对于nullptr的接受程度,那只能另当别论。

对于编译器,Visual Studio 2010已经开始支持C++0x中的大部分特性,自然包括nullptr。而VS2010之前的版本,都不支持此关键字。

Codeblocks10.5附带的G++ 4.4.1不支持nullptr,升级为4.6.1后可支持nullptr(需开启-std=c++0x编译选项)

其余编译器对于nullptr的支持就不是太清楚了。有需要的可自行翻阅Wiki。

考虑到目前的编译环境,用nullptr全面代替NULL不太现实,不过可以在文件头加入说明或使用预编译。

, , , ,

4 Comments

Win7 X64下搭建Apache+Python环境

由于要加入sunus的doubanci项目,所以需要搭建一个python的web开发环境。比较麻烦的是,不同于PHP,A&P没有现成的一体包。必须要手动安装和配置。

0.python环境

doubanci的核心代码采用python3.x,所以本机装的也是python3.x.考虑到x64的python的兼容性问题,所以装的是x86版本

IDE采用VIM和VS with PTVS。不过目前编码还是以vim为主。

1.apache

apache是个大头,曾经搭建php环境时就让我焦头烂额。

由于官方没有提供x64的apace,所以用的是非官方编译的版本,可以在http://www.blackdot.be/?inc=apache/binaries下载

该安装包使用方法如下:


0.首先解压,例如:D:\httpd-2.2-x64

1.打开conf文件夹下的httpd.conf文件(最好先备份),做如下修改(搜索前面的keyword)
   (i)  ServerRoot "/httpd-2.2-x64"
   (ii) Listen 80
   (iii)ServerName 127.0.0.1:80
   (iv) DocumentRoot "/httpd-2.2-x64/htdocs"
   (v)  定位到<Directory "/httpd-2.2-x64/htdocs">,往其中加入
        Options +ExecCGI
	AddHandler cgi-script .cgi .py

2.保存文件后在命令行里进入到D:\httpd-2.2-x64\bin,输入
  httpd -k install
进行安装。成功后会有提示

要开启apache服务,执行

httpd -k start

关闭则用

httpd -k shutdown

打开浏览器,输入http://localhost后如果看到It works则说明apache在正常运转

事实上,也可以先安装apache,在配置conf文件。

另外,步骤1中的(v)的目的是开启CGI功能,详细信息请参考http://httpd.apache.org/docs/2.0/howto/cgi.html

 

2.利用CGI解析python

最后一步相比较为简单,在apache目录的htdocs文件夹下新建一个python文件(也可以通过修改DocumentRoot将目录文件夹设为其他文件夹)

在文件头两行输入

#!D:/Python32/python.exe
print("Content-type: text/html\n\n")

这两行代码用于设置解释器位置和重定向,需要由CGI执行的文件都要包含此语句。

剩下的代码可以随便写,例如直接通过标准i/o流写html标签

print("<h1>Hi, all</h1>")
print("<h1>This is a demo for cgi with python</h1>")

保存后打开浏览器访问即可以看到结果

至于MySql什么的,暂时还没有需求,故没有安装

, , ,

7 Comments

第一个python小demo

在伟大的政治局常伪sunus童鞋鼓动下,终于开始玩python。

其实在高三毕业那会儿就有玩python的打算了,只是木有足够的决心,未能付诸实践。直到现在手头还有本从图书馆借的Begin python 2rd edition for python 2.x

直到前段时间看了sunus童鞋做的东西,以及为了把妹的光明未来,毅然决然地开始python之旅…

以下省略250句废话。

其实这个demo是山寨了书上的一个实例。山寨原因很简单,以后可能需要接触Linux+Apache+MySql+Python架构的东西。

功能很简单,从一个指定的数据源,此处是CSV文件,读取数据并格式化后重导定向输出到一个html文件,数据以表格形式呈现。

数据源信息直接从书上借的。。

另外,实例代码中这种前面是函数实现,最后一个main调用的形式让我想到了以前写的vbs

import sys

def main():
	MAX_COUNT = 100
	isHeadline = True
	print("<table border='1'>")

	while True:
		try:
			lineContent = input()

			# the color of the headline is unique
			color = "white"
			if True == isHeadline:
				color = "lightgreen"
				isHeadline = False

			PrintLine(lineContent, color, MAX_COUNT)
		except EOFError:
			break;

	print("</table>")

def PrintLine(line, color, maxCount):
	print("<tr bgcolor='{0}'>".format(color))

	fileds = ExtractFields(line)
	for filed in fileds:
		if not filed:	# the cell is empty
			print("<td></td>")
		else:
			try:
				number = int(filed)
				print("<td align='right'>{0:d}</td>".format(number))
			except ValueError:
				if len(filed) <= maxCount:
					filed = EscapeHtml(filed)
				else:
					filed = EscapeHtml("{0}...".format(filed[:maxCount]))
				print("<td>{0}</td>".format(filed))
	print("</tr>")

def ExtractFields(line):
	fileds = []
	filed = ""
	quoteChar = None

	for c in line:
		if c in "'\"":
			if None == quoteChar: # the start quote
				quoteChar = c
			elif quoteChar == c: #the end quote
				quoteChar = None
			else:
				filed += c
			continue

		if None == quoteChar and "," == c: # end of filed
			fileds.append(filed)
			filed = ""
		else:
			filed += c
	if filed:
		fileds.append(filed)

	return fileds

def EscapeHtml(text):
	text = text.replace("&", "&amp;")
	text = text.replace("<", "&lt;")
	text = text.replace(">", "&gt;")

	return text

main()

效果图:

, , , ,

12 Comments

更改内存分配策略改善归并排序效率

归并排序是一种相当稳健的排序算法,无论何种输入序列,其期望时间复杂度和最坏时间复杂度都是Θ(nlogn),这已经达到了基于比较排序算法的渐进下界。

因此归并排序时常会用于对可能导致quicksort退化的序列排序。

归并排序是典型的分治算法,一个最常见的实现如下:

void MergeSort(int a[], int low, int high)
{
	if (low < high)
	{
		int midIndex = (low + high) >> 1;
		// take apart
		MergeSort(a, low, midIndex);
		MergeSort(a, midIndex + 1, high);

		Merge(a, low, midIndex + 1, high);
	}
}

void Merge(int a[], int lowFirst, int highFirst, int highLast)
{
	int l = lowFirst;
	int r = highFirst;
	int len = highLast - lowFirst + 1;

	int* pBuffer = new int[len];
	_ASSERT(pBuffer != NULL);
	int* p = pBuffer;

	while (l < highFirst && r <= highLast)
	{
		*p++ = (a[l] <= a[r]) ? a[l++] : a[r++];
	}

	while (l < highFirst)
	{
		*p++ = a[l++];
	}

	while (r <= highLast)
	{
		*p++ = a[r++];
	}   

	p = pBuffer;
	for (int i = lowFirst; i <= highLast;)
	{
		a[i++] = *p++;
	}

	delete [] pBuffer;
}

但是在实践中,归并排序花费的时间往往超过预期,对于普通的序列而言,所花费的时间甚至远远超过quicksort。

究其原因,和归并排序的内存策略有关。

归并排序不是原地排序,需要额外的存储空间。并且在每次Merge过程中,需要动态分配一块内存以完成对两个数据堆的排序合并。并且排序完毕之后,我们需要将存储空间中的数据复制并覆盖原序列。

最后一步操作是由归并排序自身性质决定,无法优化,所以我们只能针对Merge操作。

经过分析很容易知道,对于长度为n的序列,要执行logn次的merge操作,这意味着需要进行logn次的内存分配和回收。内存操作开销较大。
Read the rest of this entry »

, , , ,

13 Comments

C++中的接口继承和实现继承

很多人认为,C++中是不存在接口继承的,只有Java、C#这类语言才提供了相应的语法支持。

但是,如同鲁迅说过的某句名言:世上本没有接口继承,用的人多了,才有了接口继承。C++中依然可以实现接口继承,只是形式上稍有不同罢了。

C++中的继承基于一个事实:父类定义的成员函数会一直被子类继承(包括被子类隐藏的部分)。

而父类中提供的函数可以有三种:1)普通成员函数 2)普通虚函数 3)纯虚函数。这三种函数类型代表了三种继承设计模式。

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

class Shape
{
	public:
		virtual void Draw() = 0;
		virtual int GetError();
		int GetId();
};

class Rectangular : public Shape
{
	//...
};

class Circle : public Shape
{
	//...
};

普通成员函数由父类声明且实现,子类应继承接口以及强制性的实现

这几乎是最常见的一种函数类型,代表了典型的”is-a”继承设计模式。

ps:所谓的”is-a”设计模式,指的是”everything that applies to base classes must also apply to derived classes

示例中,函数GetId严格遵守”is-a”模式。因为每个子类本质都是一个Shape对象,都有一个唯一的ID

普通虚函数可以在父类中有默认的实现,而这个默认实现可以由子类继承。
Read the rest of this entry »

, , ,

2 Comments

某道无聊比赛题

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

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

用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