読み込んでいます...
2010年04月17日

最近看到了一道非常有趣的排序问题,这个问题可以大概描述成这样,我们首先看下图。

这道题是这样的,在图书馆里面,有一个图书管理员需要针对一些图书进行排序,方便管理,但是排序的方法有很多,这个管理员就想到了这样一种方法,如上图所示。图书本来的序列是混乱的,最后我们期望得到的结果序列是有序的,但是这个排序的方法却和普通的排序不太相同,这个方法具体为:管理员拿出其中的一本书,然后按照这个书的序列号,放到相应的为止,其新位置的相关的书籍也随着移动(如果放到前面去了,后面的相应位置的书籍全部后移,如果放到后面去了,则前面的相应位置的书籍全部前移),一直重复这样操作,直到整个书本有序为止。

仔细看看上面的图和解释,理解这个方法应该没有多大的问题,但是问题来了,这样操作到底会不会出现死循环呢,会出现这种可能,所以到底这样排序对不对,还需要用大脑和数据来证明。

下面我给出一段代码来解释,希望能够提供到一些思路。

int a[10];

int init();
int sort();
int print();
int getright();

int main(int argc, char** argv) {
    init();
    print();
    cout
<<"============"<<endl;
    sort();
    print();
    
return 0;
}

int init(){
    a[
0] = 3;
    a[
1] = 1;
    a[
2] = 2;
    a[
3] = 5;
    a[
4] = 4;
    a[
5] = 9;
    a[
6] = 7;
    a[
7] = 8;
    a[
8] = 6;
    a[
9] = 0;
    
return 0;
}

int getright(){
    
for(int i=0;i<10;i++){
        
if(a[i]!=i)
            
return 0;
    }
    
return 1;
}

int sort(){
    
while(!getright())
    {
        
for(int i=0;i<10;i++)
        {
            
int value = a[i];
            
int index = i;

            if(value==index)
                
continue;

            //value大于index
            if(value>index)
            {
                
int temp = value;
                
for(int j=index;j<value;j++)
                {
                    a[j]
=a[j+1];
                }
                a[temp]
=temp;
            }
            
else if(value<index)
            {
                
int temp = value;
                
for(int j=index;j>value;j)
                {
                    a[j]
=a[j-1];
                }
                a[temp]
=temp;
            }
        }
    }
    
return 0;
}

int print(){
    
for(int i=0;i<10;i++)
        cout
<<a[i]<<endl;
}

339路过 3评论 趣题 阅读全文..
2010年02月17日

昨天晚上看了一个题目,这个题目很强,一开始在做的时候,没有考虑那么多,想来想去还是没有什么头绪,后来看到一个哥们写的题目发现特别简单,但是特别难懂,后来查了一下相关资料,哇,确实是一个强题,可能一般人来看这个题目的话,代码可能会写很多,而且效率也不算很高,但是这个标准解答还是很不错的。题目如下。

结果为0的序列:

考虑由1到n(n<=9)按递增顺序排成的序列1234…n,在他们之间加入加号,减号或者什么也不加,分别使它们做加法,减法或者将数字合并。然后求出结果,看看是否为0。

写一个程序,找出所有长度为n的结果为0的序列,返回序列的个数。

函数接口:int ResultIsZero(int n);

其中n表示序列的长度,int表示返回的序列的个数。

举例:

n = 8,返回10。

序列分别为:

1+2+3+4-5-6-7+8 = 0;

1+2+3-4+5-6+7-8 = 0;

1+2-3+4+5+6-7-8 = 0;

1+2-3-4-5-6+7+8 = 0;

1-2+3-4-5+6-7+8 = 0;

1-2-3+4+5-6-7+8 = 0;

1-2-3+4-5+6+7-8 = 0;

1+23-45+6+7+8 = 0;

12-34-56+78 = 0;

1-23-4+5+6+7+8 = 0;

如果你看明白了题目,你可以开始做这个题目了,如果你看明白了但是想了半天和我一样不会做的话,那就需要先来了解一下深度优先搜索DFS了。

了解百度百科,还有一个详细资料,关于图论的,关于迷宫的深度搜索算法。下面我给出一个迷宫的算法,迷宫如下图所示。

迷宫的代码如下:

Code

我们只有了解了迷宫的深度优先的搜索的递归算法,才能再来回头看我们前面说的题目,那个找结果为0的数字的表达式集合,我们同样可以用深度优先搜索计算,代码如下所示。

Code

这个答案是不是很强,其实其中还有很多值得我们去学习。。

2010年02月16日

今天一个朋友发来一道题,初步看上去好像蛮有挑战的,但是仔细想了一下之后其实好像没那么难,好像也没什么特别的陷阱,琢磨了一下,貌似也真的没什么好检查的,所以觉得也不是很难,但是朋友发的,所以就贴上来告知一下答案了。。哎,其实也没什么好些的,放假的时候无聊的朋友也可以练一下小手。。

已知f[]和g[]两个数组,大小分别为m和n,其中的元素都已经从小到大排列,且每个数组内的元素都各不相同。

例:f[5] = {1,3,4,7,9}, g[4] = {3,5,7,8}; 因为只有2组元素相同,分别是f[1]与g[0] 和 f[3]与g[2],所以函数Count的返回值应为2。

C++代码如下:

Code
2009年12月13日

题目:从M个数中随机的选取N个数。

提示:

  • 遍历一次(至少要在O(n))。
  • 保证随机(数学上概率相等)。
  • 不允许使用随机函数(不是不允许使用,但是注意,不允许在M个数里面利用随机函数随机的取N个数,那就没意义了)。

同样,我把C#/C++代码发在下面。思路在C#代码里面,可以先看思路。这也是我自己和朋友同事讨论的一个方法,不是最好,欢迎讨论。

C# Code
2009年12月7日

第一就是关于内存的速度的,其实不难,但是开发的时候想到的很少,能想到的优化的就是高人,我也是路过看到不错,觉得很有用,就贴过来了。

另一个就是《算法导论》了,国外的教材,也是MIT的标准教材,很好很强大,昨天看到了堆排序和快速排序(100页),用了好多张纸证明线性比较排序的上界和下界,堆排序和快速排序的最好的速度是nlogn,从数学角度上证明了线性比较排序(如判断a[1]>a[0])的最快就是nlogn,也就是说只要你是比较的,而且还是线性的,nlogn就是最快的了。

数学公式太复杂,什么期望啊,概率啊,极限啊,记的不是很清楚,让我自己推倒也没这个能力,不过倒是让我知道了,以后别想线性快速的排序还能超过nlogn了。:)

2009年12月3日

前面有一篇文章里面我写了LRU算法,其中还将一些算法更新了一下,现在我就做个笔记,写一下LRU算法的一些用途,另外说明一下,这里我们LRU算法都不会考虑空间不足的问题(因为从软件模拟来说基本不会存在这个问题),如果在CPU硬件设计上出现空间不足的话,我们直接将链表的第一个元素剔除即可,当然,也有相应硬件能够直接做这种事情。

当然,LRU不仅仅在硬件设计上有很好的作用,在软件设计上也有很好的作用,例如Orical的数据库就是用了LRU算法。我们可以先看看下面这个图。

Oracle的设计可以从上图看到,这里buffer(缓冲区)就像一个水池,水池的最小单位就是数据块。当每个数据块被读入buffer时,Oracle或相应的软件都会抽取数据块的头部,在内存中构建buffer header,并将这些buffer header串成链表的形式。而buffer header里面记录的指针就指向buffer中的该数据块本身。于是,当我们在搜索某个数据块时,就不用去缓冲区中找,而是直接扫描链表上该数据块所对应的buffer header,然后根据找到的buffer header所记录的指针就能到buffer中直接定位该数据块了。而维护buffer header就是hash表,而buffer header就是一个LRU维护的链表。

这里的buffer head就是数据的一些信息,例如如果我将数字存储到数据中,我可以通过一种方式获取其head的信息,存放到相应的hash组里面,而hash的key也是可以通过数据本身得到。例如我们可以将数字求100的余数得到key,然后余10得到页面id。例如25这个数字,我们可以余100为25,余10为5,那么我们就在缓冲区buffer[25,5]的地方将这个数据保存起来,下次我们取的时候可以直接从25获得hash的组id和存放的页面id,即25和5,则我们直接读取buffer[25,5]即可。

说了这么多,可以稍微总结一下,这个数据结构就是。

  • hash表:存放数据的组,用于查找和维护。
  • buffer head:LRU链表,被hash组维护,其内容为在内存(缓冲区)的页面id。
  • buffer:缓冲区,用于存放数据。

简单的说,也就是当数据读入,我们通过一种方法获取组的id,然后在相应的组中维护一个页面id(链表),然后查找的时候直接通过组id和页面id得到数据,从而提高速度。

不过这里就会有一个问题,hash表这里就不存在冲突的问题了,因为hash表的value是一个链表结构,所以不存在冲突,如果有相同的key的话,我们直接在后面添加节点即可,但是在缓冲区的话就有可能有问题,例如25和125这两个值最后在缓冲区的位置应该是一样的,那么我们肯定不能覆盖掉,所以我们就查找25这个位置(25,5)后面的这个空间是否被使用(25,6),如果没被使用,则我们写到这个空间中,并更改LRU链表指针里面的值。

例如25会是第25组,其页id为5,当读到125的时候,发现也是25组和id为5,那么我们就查找6的位置,如果6位置没数据写入,那么OK,写到6的位置,并更新链表为第25组,页id为6,直到内存缓冲区被写满为止,我们都不覆盖

这样我们解决了冲突问题,但是这样的话,查找也就出了问题,即不能够直接查找,同样是25和125这两个数,两个坐标最后会成为(25,5)和(25,6),但是我们两个数通过规则获得到的key和页面id是25和5。所以当我们查找的时候就不需要使用页面id,而是采用扫描的方式。即当我们搜索125这个数字的时候,可以获得组id,即25,然后扫描这个组维护的LRU链表,然后扫描内存块,查找是否是我们需要的数据,如果是,则返回。就是我们搜索125,发现组id为25,那么我们就找(25,5),发现数据为25,不是我们想要的,继续下一个链,(25,6)直到找到需要的数据。

不过上面我们也可以优化一下,即优化成结构体,每个数据都有自己的独立的页面id,而不是通过一种规则去找页面id,尽量避免冲突,提高效率。我们可以看一下Orical的做法。

“当前台进程发出SELECT或者其他DML语句时,Oracle根据SQL语句的执行计划找到符合SQL条件的数据块,然后Oracle会根据对请求的数据块的地址以及数据块的类型作为参数,应用hash函数以后,得到要找的数据块所处的hash bucket,也就是确定该数据块在哪条hash chain上。然后,Oracle进入该hash chain,从上面所挂的第一个buffer header开始,根据buffer header所含有的指针找到对应的块体,然后扫描其中的数据,确认其是否是SQL语句所需要的块,如果是,则返回该块里所需要的数据;否则,如果不是,则继续往下搜索,一直搜索到最后一个buffer header为止。如果一直都没有找到,则调用物理I/O,到数据文件里把该块所含有的内容复制一份到一个可用的buffer里,并构建该块的 buffer header,然后将该buffer header挂到hash chain上去。”

所以基本上我们可以明确算法了,下面我给一下C#模拟的代码。

namespace LRUandData
{
定义LRU链表

class Program
{
定义数据块,缓冲区和hash表

static void Main(string[] args)
{
hash
= new Dictionary<int, LRUList>();
//初始化数据
InsertData();
//查询数据
int value = SearchData(33);
Console.WriteLine(value);
Console.ReadKey();
}


初始化数据

查找数据
}

}

上面的代码模拟了基本的操作,如果有更多功能只能大家自己改了。:)

从这里我们也可以看到,设计一个好的软件,数学,数据结构,计算机组成原理等等基础的东西是非常重要的,决定技术高度,基础的牢固程度最重要,举个老师给我们举了一百遍的例子,房子的高度由地基牢固程度决定,真的一点不假。

2009年11月26日

昨天同事说了一道题,这道题说难也难,说简单也简单,但是细节比较麻烦,是个难题,如果这个是面试题的话,确实能够拦下很多人。

题目:有一串字符串,根据字符串生成二叉树,输入输出如下(输出是以树的形式输出)。

输入:{1}

输出:1

输入:{1{2,3}}

输出:根节点1,1的左节点为2,1的右节点为3

输入:{1{2{4},3{5,6}}}

输出:根节点1,1的左节点为2,2的左节点为4,右节点为空,1的右节点为3,3的左右节点分别为5,6。

下面是代码,慎看,并不一定是最好的,与标答还是有一定的距离的,不过效率还不错,标答就比较难理解了,过一段时间我再把标答发上来。思路不是很难,但是代码估计很难看懂:(

生成树

标答的技巧很巧妙,过一段时间让朋友帖上来。

2009年11月23日

今天在逛鲁攀的QQ空间,看到一个很有意思的题目,也从另一方面告诉我们了编程中数学的重要性,很多时候数学能够给好的算法和高效的速度提供灵感,例如一个非常复杂的问题,我们却可以用几步就可以实现。

在看到这个题目之后,我就在网上查了一下这个题目并看到了异或运算的神奇,因为在说到xor异或运算的时候,大部分人都不会认为xor异或运算有什么有意思的事情,但是xor异或运算真的很神奇,不如我们首先先看个游戏,这个游戏的名字叫Nim游戏。而我们要看到的就是Nim游戏的必胜法则。(即在何种情况下无论对手如何走自己都会必胜)

有若干堆石子,每堆石子的数量都是有限的,合法的移动是“选择一堆石子并拿走若干颗(不能不拿)”,如果轮到某个人时所有的石子堆都已经被拿空了,则判负(因为他此刻没有任何合法的移动)。

这个游戏貌似是很老很老的游戏,但是直到20世纪初才被一位数学家所发现规律,所以难度可见一斑,Nim游戏是博弈论中最经典的模型之一,所以现在我们当下的人去写Nim游戏会发现其实很简单,但是我们都是站在巨人的肩膀上去看问题的。从百科里我们可以知道如下内容:

Nim游戏是组合游戏(Combinatorial Games)的一种,准确来说,属于“Impartial Combinatorial Games”(以下简称ICG)。满足以下条件的游戏是ICG:

  • 有两名选手。
  • 两名选手交替对游戏进行移动(move),每次一步,选手可以在(一般而言)有限的合法移动集合中任选一种进行移动。
  • 对于游戏的任何一种可能的局面,合法的移动集合只取决于这个局面本身,不取决于轮到哪名选手操作、以前的任何操作、骰子的点数或者其它什么因素。 如果轮到某名选手移动,且这个局面的合法的移动集合为空(也就是说此时无法进行移动),则这名选手负。

大家可以仔细思考一下,如何才能再能自己必胜呢?

首先我们还是得回到xor运算里面来,xor运算有几个非常有趣的规律:

  1. 异或自己是自己的逆运算。
  2. 异或运算满足消去率。

第一个我们可以从一个非常经典的题目中看到,这个题目就是交换两个数,写一个swap函数。我们普通会写成。

static void Main(string[] args)
{
int a = 1;
int b = 2;
a
= a + b; b = a - b; a = a - b;
Console.Write(a);
Console.Write(b);
}

但是还有一个经典答案就是用异或。

static void Main(string[] args)
{
int a = 1;
int b = 2;
a
= a^b; b = a^b; a = a^b;
Console.Write(a);
Console.Write(b);
}

貌似更好理解,而且咱们也不必去记是加还是减,统统异或(而且计算机操作异或操作肯定比加减法要快)。

第二个满足消去率,即a^b=c^b,我们可以知道a=b,这是在and,or运算中没有的,所以也具有这个神奇的性质。

所以同Nim游戏,也有一个非常有趣的题目,这个题目就是找到2n+1数中的单独的数字,其中相同的数字有2n个,也就是有n对,比如{1,2,3,4,5,4,3,2,1},而且这个n很大,如何找到这个n也是非常奇妙的,从这两题,我们可以知道xor异或运算符的神奇了。

对于Nim游戏,我们就是要找到对手的必败态,必败态就是当出现某种局面的时候,对手无论如何走都会输。达到必败态有两种情况,1是直接走到必败态,2是走n步比到必败态。所以我们这里就要找到Nim游戏的必败态。而我前面说了,咱都是凡人,所以直接看别人的研究成果吧。

Nim游戏是必败态当且仅当所有堆硬币的数量都异或起来结果为0,即a1^a2^…^an=0。

证明很简单,但是我不是搞数学的,所以就不贴出来充数了,可自行百科。所以在和别人玩Nim游戏或博弈的游戏的时候,我们就必须找到一种必败态,来让对手无论如何走都会输。

从Nim游戏我们可以看出来,xor运算也确实是非常神奇的,而且在很早时候就出了这个游戏,而在计算机理论之后诞生的时候,这个游戏才慢慢的被揭开,是不是从另一方面说明了2进制更符合自然语言的说法呢(原来看的某本数学书中说的,无法考究)。

当然,因为异或自己是自己的反操作的这个特性,我们也可以回过头来看找独数的这个题{1,2,3,4,5,4,3,2,1},这个题无论我们怎么遍历都会是n2,另一种算法就是讲数字存放到hash表里,key就是数值,value就是匹配的数,1,1匹配,value就是0,则最后这个hash表里面会存放{1,0}{2,0}..{5,1},这样我们就可以找到独数,但是浪费了空间,虽然复杂度也是O(n)(实际上可以说n+n/2加上比n大许多个空间)不过既然xor很神奇,不如我们全部异或一下。

1^2^3..^n..^3^2^1

嗯,看来我们只要异或一下,答案就出来了。看来异或运算真的很神奇,也让我感觉到,自己离计算机科学与技术这门学科,只是刚刚敲开了大门而已。

PS:最后还有一题:)

一堆石头有n個,两个人轮流取石,每次每人至少取一個,最多取上次对方取走石头的三倍。最後取光的贏得游戏。當然,第一個拿的人不可以第一次就取光所有石頭。

第一问:
当1 <= n <= 100
结果为何
第二问:
当1 <= n <= 1000
结果为何
第三问:
当1 <= n <= 1000000
结果为何

748路过 1评论 趣题 阅读全文..
2009年11月18日

今天在看厕所专刊(编程珠玑)的时候看到了粗略估算,有点感悟,所以写下关于粗略估算的笔记。

粗略估算在工程角度来说是非常重要的,对于个人的能力评估也是很重要的,粗略估算不是说和仔细的数学的计算正确结果相悖,而是在某些情况下,在得到正确的结果之前,能够给我们一个很准确的方向,例如下面这道题。

题目:从N个数中取M个最大值。N是无序的,不连续的。

当然,在做上面这道题的时候,大家都可以很容易的做出来,例如快速排序,取前M个数,用二叉排序树,后序遍历M个数,堆排序,等等,不是很复杂。当然,这个题还可以变形一下,如下。

题目:从100万个数中取最大的10个数。

好吧,我想大部分的人都会犯这样一个错误,100万?这么大,那么我怎么组织空间才能性能最好,速度最快呢?其实,这个肯定是有一定问题的,100万,不是小数目,但是我们不妨计算一下,在各个系统中占用的内存最坏是多少,我们可以大约估算一下,在16位的机器里,一个指针和一个整数大约占4个字节,32位,那么就是8字节,那么64位就是16字节。那么100万个32位的整数和指针,大约会占用8M的内存,也就是说,在现在的计算机里,就算你将100万个数全部放到内存中,也才8M,那么,最坏最坏才8M,你就可以为你的程序计算出最坏的情况,比如这个程序需要10M内存,CPUx%以及进程数n,那么最后的程序应该在这个预期之内或更好。为了追求速度,可以牺牲一点空间,可以全部读入,如果要求空间有限,那就要优化一下,只用4M或2M内存或者更少。

当然这个题目你可以进行插入排序维护M个节点,这样你可以节约很多空间,只要维护N个字节即可,但是维护节点的性能上也会有很大的损失,100万个数字要维护线性次数,也不是一个小数目,如果你内存需求不是很高,可以一次读入,也会很快。

所以估算能力也是非常重要的,例如我们项目需要写一个程序,需要算法若干,功能若干,我们可以大概的估算一下我们所需要的内存是多少,例如10M,估算一下CPU使用率,大概不会超过5%,估算一下时间,如发送一个报告或者查找一个文件需要0.1秒,发送100个报告大概需要10秒。那么整个系统自动化跑下来可能总共花20M内存,2分钟,线程2个,嗯,是不错的结果。然后在开发过程中,如果大约估算是这个估算结果以内的话,我们可以说程序达到了一个预期的目的,如果再次优化的优先级不高(因为在估算内,肯定是与客户交流过),已经是不错的程序了。当然,程序还是会有很多优化手段的,这里不再做过多的说明了,毕竟任何程序都是可以优化的。

Little定律

编程珠玑里面降到了Little定律,我觉得是非常有必要再拿出来一说的。Little定律指出“系统中物体的平均数量等于物体离开系统的平均速率和每个物体在系统中停留的平均时间的乘积”,所以我们也可以看到这个例子,“这个地方可以容纳约60个人,每个人在这里面逗留的时间大约是3小时,所以我们可以看到进入这个地方的速率大约是每小时20人,现在前面的队伍中还有20人,所以我们估计还需要等待1个多小时”。

同样的我们可以计算一下我有100瓶矿泉水,我每天喝掉2瓶并买入2瓶,那么每瓶矿泉水保存的时间是多少天?可以估算一下100/2就是50天,平均情况下,每瓶矿泉水都可以保存50天(100=2*x=>x=50)。所以Litte法则也可以简单的说成“队列中物体的平均数量为进入速率与平与停留时间的乘积”。

在计算机系统中,这个定律也是有一定的估算作用的,例如在一个系统队列中,平均每秒处理n个数据,这个数据会在队列中停留t时间,而系统的响应时间为r,而整个系统队列中的总数为x,我们就可以得到一个简单的式子,那就是(根据Little定律得到)x=n*(t+r),即进入队列平均数量为进入的速率与平均停留时间的乘积,而这个估算是相当精确的。

所以在程序开发中,能够写出好的程序是第一步(得到正确的结果和不错的速度),第二步就是在一定的情况下的在一定的成本和需求下进行优化(如1天内速度从50秒达到10秒,虽然可以优化到5秒,但是不是非常必要的,因为10秒对客户已经足够友好)。虽然以上两点已经足够,但是在程序开发之初,我们能够通过粗略估算计算出程序的性能为10秒的话,我们可以在设计之初就能够判断我们的设计是否正确,而不是在后期去进行优化,当第一个版本的程序已经能够满足客户需求的时候,我们就不需要额外的成本再去优化以达到某种程序的性能要求了。(不过通常优化的速度越快越好:))

2009年11月16日

题目:一个好汉三个帮,三个好汉九个帮,九个好汉二十七个帮,…停,今天不是让你算n个好汉几个帮(有兴趣的网友可以自己算),我们来看这个集合

{ 1, 3, 9, 27, 81, … }

这个集合是一个递增的无限集,每个元素都是3的幂。

我们把这个集合的所有子集按照元素和从小到大的顺序排好: {}(空集), { 1 }, { 3 }, { 1, 3 }, { 9 }, { 1, 9 }, { 3, 9 }, { 1, 3, 9 }, …

现在给定一个正整数n,求第n个子集的所有元素,并按从大到小的顺序排列好

例:

n的值    结果
1        { }
4        { 3, 1 }
6        { 9, 1 }
500      { 6561, 2187, 729, 243, 81, 3, 1 }

题目:给出一个包含各种括号的表达式,判断括号是否配对。括号配对的条件:括号必须先左后右,并且左右括号数量相等;对于多重括号,从外到内嵌套顺序为:{} -> [] -> () -> <>。例如{[(<>)]}。

接口: bool match(char* str);   合法返回true, 否则返回false。

输入范例:
{[(<>)]}
{}
<(>)
<()>

返回:
true
true
false
false

来自编程爱好者编程比赛题。

Answer1