FreeStyle分享 第二期

FreeStyle 第二期 树与链表在淘宝笔试题中的解决方法与害死人的笔试题

首先,树是一种数据结构,它是由nn>=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种解法,时间/空间复杂度的分析还不是很清楚,希望大家帮助解答。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值