FreeStyle第二期树与链表在淘宝笔试题中的解决方法与害死人的笔试题
首先,树是一种数据结构,它是由n(n>=1)个有限结点组成一个具有层次关系的集合。
树具有2种表示方法,也就是常说的双亲表存储和孩子链表存储。
让我们先来看下这2种表示法的实现
#################################################################
/*树的双亲表存储表示*/
#defineMAX_TREE_SIZE100
typedefstruct
{
TElemTypedata;
intparent;/*双亲位置域*/
}PTNode;
typedefstruct
{
PTNodenodes[MAX_TREE_SIZE];
intn;/*结点数*/
}PTree;
#################################################################
/*树的孩子链表存储表示*/
typedefstructCTNode{//孩子结点
intchild;
structCTNode*next;
}*ChildPtr;
typedefstruct{
ElemTypedata;//结点的数据元素
ChildPtrfirstchild;//孩子链表头指针
}CTBox;
typedefstruct{
CTBoxnodes[MAX_TREE_SIZE];
intn,r;//结点数和根结点的位置
}CTree;
#################################################################
这里对一些基本概念就不多说了。下面通过一些场景来深入了解树对常见问题的处理方式
有一颗结构如下的树,对其做镜像反转后如下,请写出能实现该功能的代码。注意:请勿对该树做任何假设,它不一定是平衡树,也不一定有序。
11
/|\ /|\
234432
/|\/\||/\/|\
65789101098756
有了前面的介绍,很容易看出,这个题目可以用孩子链表存储结构来存储这棵树,然后对孩子链表内部进行链表反转,对节点的处理可以用递归处理。
下面是一份网上流传开的参考代码,虽然他存在缺陷,但是大体上还是实现了我们想要的结果。
让我们一起来分析一下。
typedefstructTreeNode
{
intdata;
structTreeNode*firstchild;
structTreeNode*nextsibling;
}TreeNode,*Tree;
voidMirrorTree(Treeroot)
{
if(!root)
return;
if(root->firstchild)
{
Treep=root->firstchild;
Treecur=p->nextsibling;
p->nextsibling=NULL;
while(cur)
{
Treecurnext=cur->nextsibling;
cur->nextsibling=p;
if(p->firstchild)
MirrorTree(p);
p=cur;
cur=curnext;
}
root->firstchild=p;
}
}
intmain(void)
{
TreeNode*root=(TreeNode*)malloc(sizeof(TreeNode));
Init();
MirrorTree(root);
OutPut();
}
我们只看关键部分也就是从if(root->firstchild) 开始的那段,下面我们带入一组数据来更加直观的来说明这个问题
构建一棵树如下:(A是根节点,BCDE为同层的叶子)
A----
|------B
|------C
|------D
|------E
那么这棵树反转以后就是
A----
|------E
|------D
|------C
|------B
然后,我们走一边代码,看看效果
从if(root->firstchild)开始,此时
|
步骤 |
节点名 |
值 |
->nextsibling |
链表 | ||||
|
第一行 |
root->firstchild |
B |
C-D-E |
B-C-D-E | ||||
|
第二行 |
p |
B |
C-D-E |
B-C-D-E | ||||
|
第三行 |
cur |
C |
D-E |
C-D-E | ||||
|
第四行 |
p |
B |
NULL |
B | ||||
|
第五行 |
cur |
C |
D-E |
C-D-E | ||||
|
第六行 |
curnext |
D |
E |
D-E | ||||
|
第七行 |
cur |
C |
B |
C-B | ||||
|
第八行,第九行 |
B节点进行递归(这里的B节点是叶子,所以直接返回) | |||||||
|
第十行 |
p |
C |
B |
C-B | ||||
|
第十一行 |
cur |
D |
E |
D-E | ||||
|
第五行 |
cur |
D |
E |
D-E | ||||
|
第六行 |
curnext |
E |
NULL |
E | ||||
|
第七行 |
cur |
D |
C-B |
D-C-B | ||||
|
第八行,第九行 |
C节点进行递归(这里的C节点是叶子,所以直接返回) | |||||||
|
第十行 |
p |
D |
C-B |
D-C-B | ||||
|
第十一行 |
cur |
E |
NULL |
E | ||||
|
第五行 |
cur |
E |
NULL |
E | ||||
|
第六行 |
curnext |
NULL |
NULL |
NULL | ||||
|
第七行 |
cur |
E |
D-C-B |
E-D-C-B | ||||
|
第八行,第九行 |
D节点进行递归(这里的D节点是叶子,所以直接返回) | |||||||
|
第十行 |
p |
E |
D-C-B |
E-D-C-B | ||||
|
第十一行 |
cur |
NULL |
NULL |
NULL | ||||
|
第十二行 |
root->firstchild |
E |
D-C-B |
E-D-C-B | ||||
细心的人一经发现问题了,按照代码走下来,到最后一步,整个镜像已经结束,但是对于E节点(这里是叶子),并没有做镜像,那么应该怎么解决呢?
方法很简单,只需要在第十一行与十二行之间加入if(p->firstchild)MirrorTree(p)
于是就有了
|
新加入行 |
E节点进行递归(这里的E节点是叶子,所以直接返回) |
这样就是一个完整的利用孩子链表存储对树的镜像的解决方案了。
在这里补充一个阿里云关于链表反转的笔试题
一个链表是这样的:1->2->3->4->5通过反转后成为5->4->3->2->1
structlinka{
intdata;
linka*next;
};
voidreverse(linka*&head)
{
if(head==NULL)
return;
linka*pre,*cur,*ne;
pre=head;
cur=head->next;
while(cur)
{
ne=cur->next;//填空1
cur->next=pre;//填空2
pre=cur;//填空3
cur=ne;//填空4
}
head->next=NULL;
head=pre;
}
虽然2段代码的实现有点类似,但是仔细看了还是有区别的,那么我在这里再过一遍流程,
上一题没看懂的可以通过这个了解一下。这里的关键代码还是从pre=head开始吧
为了简单起见,我们把链表修改为1-2-3
|
步骤 |
节点名 |
值 |
->next |
链表 | |||
|
第一行 |
pre |
1 |
2-3-4 |
1-2-3-4 | |||
|
第二行 |
cur |
2 |
3-4 |
2-3-4 | |||
|
第三行 |
cur |
2 |
3-4 |
2-3-4 | |||
|
第四行 |
ne |
3 |
4 |
3-4 | |||
|
第五行 |
cur |
2 |
1-2-3 |
2-1-2-3 | |||
|
第六行 |
pre |
2 |
1-2-3 |
2-1-2-3 | |||
|
第七行 |
cur |
3 |
4 |
3-4 | |||
|
第三行 |
cur |
3 |
4 |
3-4 | |||
|
第四行 |
ne |
4 |
NULL |
4 | |||
|
第五行 |
cur |
3 |
2-1-2-3-4 |
3-2-1-2-3-4 | |||
|
第六行 |
pre |
3 |
2-1-2-3-4 |
3-2-1-2-3-4 | |||
|
第七行 |
cur |
4 |
NULL |
4 | |||
|
第三行 |
cur |
4 |
NULL |
4 | |||
|
第四行 |
ne |
NULL |
NULL |
NULL | |||
|
第五行 |
cur |
4 |
3-2-1-2-3-4 |
4-3-2-1-2-3-4 | |||
|
第六行 |
pre |
4 |
3-2-1-2-3-4 |
4-3-2-1-2-3-4 | |||
|
第七行 |
cur |
NULL |
NULL |
NULL | |||
|
第八行 |
Head |
1 |
NULL |
1 | |||
|
第九行 |
Head |
4 |
3-2-1-2-3 |
4-3-2-1-2-3-4 | |||
写到这里,是不是又发现问题了呢?简单的,所作的修改仅仅是在while循环之前加入一句pre->next=null就可以解决这个问题。然后我们可以得到一张新的流程图(弱弱说一句,常见的一个错误,苦了多少无辜的娃)
|
步骤 |
节点名 |
值 |
->next |
链表 | |||
|
第一行 |
pre |
1 |
2-3-4 |
1-2-3-4 | |||
|
第二行 |
cur |
2 |
3-4 |
2-3-4 | |||
|
新加入的行 |
pre |
1 |
NULL |
1 | |||
|
第三行 |
cur |
2 |
3-4 |
2-3-4 | |||
|
第四行 |
ne |
3 |
4 |
3-4 | |||
|
第五行 |
cur |
2 |
1 |
2-1 | |||
|
第六行 |
pre |
2 |
1 |
2-1 | |||
|
第七行 |
cur |
3 |
4 |
3-4 | |||
|
第三行 |
cur |
3 |
4 |
3-4 | |||
|
第四行 |
ne |
4 |
NULL |
4 | |||
|
第五行 |
cur |
3 |
2-1 |
3-2-1 | |||
|
第六行 |
pre |
3 |
2-1 |
3-2-1 | |||
|
第七行 |
cur |
4 |
NULL |
4 | |||
|
第三行 |
cur |
4 |
NULL |
4 | |||
|
第四行 |
ne |
NULL |
NULL |
NULL | |||
|
第五行 |
cur |
4 |
3-2-1 |
4-3-2-1 | |||
|
第六行 |
pre |
4 |
3-2-1 |
4-3-2-1 | |||
|
第七行 |
cur |
NULL |
NULL |
NULL | |||
|
第八行 |
head |
1 |
NULL |
1 | |||
|
第九行 |
head |
4 |
3-2-1 |
4-3-2-1 | |||
接下来是一个海量存储的问题。这个问题引入了B+树这个数据结构
B+树是应文件系统所需而出的一种B-树的变型树。一棵m阶的B+树和m阶的B-树的差异在于:
1.有n棵子树的结点中含有n个关键字。
2.所有的叶子结点中包含了全部关键字的信息,及指向含这些关键字记录的指针,且叶子结点本身依关键字的大小自小而大顺序链接。
3.所有的非终端结点可以看成是索引部分,结点中仅含其子树(根结点)中的最大(或最小)关键字。
4.通常在B+树上有两个头指针,一个指向根结点,一个指向关键字最小的叶子结点。
那么我们来看场景:
假设某个网站每天有超过10亿次的页面访问量,出于安全考虑,网站会记录访问客户端访问的ip地址和对应的时间,如果现在已经记录了1000亿条数据,想统计一个指定时间段内的区域ip地址访问量,那么这些数据应该按照何种方式来组织,才能尽快满足上面的统计需求呢,设计完方案后,并指出该方案的优缺点,比如在什么情况下,可能会非常慢?
解法:
用B+树来组织,非叶子节点存储(某个时间点,页面访问量),叶子节点是访问的IP地址。这个方案的优点是查询某个时间段内的IP访问量很快,但是要统计某个IP的访问次数或是上次访问时间就不得不遍历整个树的叶子节点。或者可以建立二级索引,分别是时间和地点来建立索引。
答案是网上提供的,从数据结构来看,B+树是这个问题比较好的解决方案。
最后是咱们的牛P柬之大神提供的一个面试题。柬之可以职业面试官哦~
柬之口头描述问题如下:给你1棵非常大的树,任意取2个节点,请你找出他们最小父节点(所有的节点的最大父节点都是根)
柬之的分析:题目并不难,但是要在短短的几分钟面试时间做出来,还是有点点难度的。
下面是柬之给出的答案:
将2个节点的所有父节点分别存储在2个数组中,然后从根节点开始做数组比较,直到2者不相等的temp=n的位置,那么他们的最小父节点就是在temp=n-1这个位置。
然后勇乔也提供了一种解法:
将深度比较大的节点的父节点存储在一个MAP命名为Tmap中,以节点的地址作为key,所有的value都为1。然后将另一个节点的所有父节点从底到根存入刚才的MAP中,如果Tmap[地址]==1,那么就命中,返回的该节点,否则就继续往根部寻找。
最后留下一个问题,对于这2种解法,时间/空间复杂度的分析还不是很清楚,希望大家帮助解答。
1146

被折叠的 条评论
为什么被折叠?



