FreeStyle 第二期 树与链表在淘宝笔试题中的解决方法与害死人的笔试题
首先,树是一种数据结构,它是由n(n>=1)个有限结点组成一个具有层次关系的集合。
树具有2种表示方法,也就是常说的双亲表存储和孩子链表存储。
让我们先来看下这2种表示法的实现
#################################################################
/* 树的双亲表存储表示 */
#define MAX_TREE_SIZE 100
typedef struct
{
TElemType data;
int parent; /* 双亲位置域 */
} PTNode;
typedef struct
{
PTNode nodes[MAX_TREE_SIZE];
int n; /* 结点数 */
} PTree;
#################################################################
/*树的孩子链表存储表示*/
typedef struct CTNode { // 孩子结点
int child;
struct CTNode *next;
} *ChildPtr;
typedef struct {
ElemType data; // 结点的数据元素
ChildPtr firstchild; // 孩子链表头指针
} CTBox;
typedef struct {
CTBox nodes[MAX_TREE_SIZE];
int n, r; // 结点数和根结点的位置
} CTree;
#################################################################
这里对一些基本概念就不多说了。下面通过一些场景来深入了解树对常见问题的处理方式
有一颗结构如下的树,对其做镜像反转后如下,请写出能实现该功能的代码。注意:请勿对该树做任何假设,它不一定是平衡树,也不一定有序。
1 1
/ | \ / | \
2 3 4 4 3 2
/| \ /\ | | / \ / | \
6 5 78 9 10 10 9 8 7 5 6
有了前面的介绍,很容易看出,这个题目可以用孩子链表存储结构来存储这棵树,然后对孩子链表内部进行链表反转,对节点的处理可以用递归处理。
下面是一份网上流传开的参考代码,虽然他存在缺陷,但是大体上还是实现了我们想要的结果。
让我们一起来分析一下。
typedef struct TreeNode
{
int data;
struct TreeNode *firstchild;
struct TreeNode *nextsibling;
}TreeNode,*Tree;
void MirrorTree(Tree root)
{
if(!root)
return ;
if(root->firstchild)
{
Tree p=root->firstchild;
Tree cur=p->nextsibling;
p->nextsibling=NULL;
while(cur)
{
Tree curnext=cur->nextsibling;
cur->nextsibling=p;
if(p->firstchild)
MirrorTree(p);
p=cur;
cur=curnext;
}
root->firstchild=p;
}
}
int main(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
struct linka {
int data;
linka* next;
};
void reverse(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种解法,时间/空间复杂度的分析还不是很清楚,希望大家帮助解答。