Archive for category PROGRAMMING
NULL和nullptr
Posted by kingsamchen in PROGRAMMING on 2012 年 02 月 06 日
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不太现实,不过可以在文件头加入说明或使用预编译。
Win7 X64下搭建Apache+Python环境
Posted by kingsamchen in PROGRAMMING on 2011 年 11 月 12 日
由于要加入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什么的,暂时还没有需求,故没有安装
第一个python小demo
Posted by kingsamchen in PROGRAMMING on 2011 年 10 月 21 日
在伟大的政治局常伪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("&", "&")
text = text.replace("<", "<")
text = text.replace(">", ">")
return text
main()
效果图:
更改内存分配策略改善归并排序效率
Posted by kingsamchen in PROGRAMMING on 2011 年 10 月 15 日
归并排序是一种相当稳健的排序算法,无论何种输入序列,其期望时间复杂度和最坏时间复杂度都是Θ(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 »
C++中的接口继承和实现继承
Posted by kingsamchen in PROGRAMMING on 2011 年 10 月 12 日
很多人认为,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 »
某道无聊比赛题
Posted by kingsamchen in PROGRAMMING on 2011 年 10 月 01 日
又是一周发文时。。。这次写篇槽文好了
题目来自上届的信息技术应用大赛复赛题,题目描述如下:
用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比较大时,符合条件的数反而更少。
另外,以上讨论的方法都是基于生成测试法。似乎此算法的下界也至少是立方复杂度(未证明)。
所以如果要设计更高效的算法的话,应该需要换一个角度。
C#的下标属性(indexers property)
Posted by kingsamchen in PROGRAMMING on 2011 年 09 月 17 日
差不多一周木有写文章了,就拿这个凑个数。。。
前几天看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#加了一层下标属性,实现起来会更方便而且强大
堆与优先级队列
Posted by kingsamchen in PROGRAMMING on 2011 年 09 月 02 日
优先级队列(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 »
C++中用qsort排序字符串
Posted by kingsamchen in PROGRAMMING on 2011 年 08 月 24 日
下午写代码时需要对一个由字符串指针构成的数组进行排序。
为了节约时间和代码量,提高重用性(嗯,你可以看作是我偷懒),决定用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**。
直接的参数转换是没法通过编译的
表达式树的简单实现
Posted by kingsamchen in PROGRAMMING on 2011 年 08 月 07 日
表达式树是树结构的一个经典应用,常用于编译器的设计。
在表达式树中,叶子通常是常数值或者变量名,统称为操作数(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 »

COMMENTS