百度质量部电话面试题

1:两个链表找公共节点:

struct ListNode 
{ 
      int     m_nKey; 
      ListNode*   m_pNext; 
}; 

分析:这是一道微软的面试题。微软非常喜欢与链表相关的题目,因此在微软的

面试题中,链表出现的概率相当高。 

 

如果两个单向链表有公共的结点,也就是说两个链表从某一结点开始,它们的

m_pNext都指向同一个结点。但由于是单向链表的结点,每个结点只有一个

m_pNext,因此从第一个公共结点开始,之后它们所有结点都是重合的,不可能

再出现分叉。所以,两个有公共结点而部分重合的链表,拓扑形状看起来像一个

Y,而不可能像 X。 

 

看到这个题目,第一反应就是蛮力法:在第一链表上顺序遍历每个结点。每遍历

一个结点的时候,在第二个链表上顺序遍历每个结点。如果此时两个链表上的结

点是一样的,说明此时两个链表重合,于是找到了它们的公共结点。如果第一个

链表的长度为 m,第二个链表的长度为 n,显然,该方法的时间复杂度为 O(mn)。  

 

接下来我们试着去寻找一个线性时间复杂度的算法。我们先把问题简化:如何判

断两个单向链表有没有公共结点?前面已经提到,如果两个链表有一个公共结

点,那么该公共结点之后的所有结点都是重合的。那么,它们的最后一个结点必

然是重合的。因此,我们判断两个链表是不是有重合的部分,只要分别遍历两个

链表到最后一个结点。如果两个尾结点是一样的,说明它们用重合;否则两个链

表没有公共的结点。 

在上面的思路中,顺序遍历两个链表到尾结点的时候,我们不能保证在两个链表

上同时到达尾结点。这是因为两个链表不一定长度一样。但如果假设一个链表比

另一个长l个结点,我们先在长的链表上遍历 l个结点,之后再同步遍历,这个

时候我们就能保证同时到达最后一个结点了。由于两个链表从第一个公共结点考

试到链表的尾结点,这一部分是重合的。因此,它们肯定也是同时到达第一公共

结点的。于是在遍历中,第一个相同的结点就是第一个公共的结点。 

 

在这个思路中,我们先要分别遍历两个链表得到它们的长度,并求出两个长度之

差。在长的链表上先遍历若干次之后,再同步遍历两个链表,知道找到相同的结

点,或者一直到链表结束。此时,如果第一个链表的长度为 m,第二个链表的长

度为n,该方法的时间复杂度为 O(m+n)。

int length(node *list){ //求长度
int len=0;
while(list){
len++;
list=list->next;
}
return len;
}
node *find_node(node *&listA,node *&listB){
int lenA=length(listA),lenB=length(listB);
node *lA=listA,*lB=listB;
if(lenA>lenB){
for(int i=1;i<=lenA-lenB;i++)
lA=lA->next;
}else{
for(int i=1;i<=lenB-lenA;i++)
lB=lB->next;
}
while(lA&&lB){
if(lA==lB){
return lA;
}else{
lA=lA->next;
lB=lB->next;
}
}
return NULL;
}

2:链表里有一个循环,求此点

有一个单链表,其中可能有一个环,也就是某个节点的next指向的是链表中在它之前的节点,这样在链表的尾部形成一环。

问题:

1、如何判断一个链表是不是这类链表?
2、如果链表为存在环,如果找到环的入口点?

解答:

1、最简单的方法, 用一个指针遍历链表, 每遇到一个节点就把他的内存地址(java中可以用object.hashcode())做为key放在一个hashtable中. 这样当hashtable中出现重复key的时候说明此链表上有环. 这个方法的时间复杂度为O(n), 空间同样为O(n). 

2、使用反转指针的方法, 每过一个节点就把该节点的指针反向

 Boolean reverse(Node *head) {

   Node *curr = head;

   Node *next = head->next;

   curr->next = NULL;

   while(next!=NULL) {

    if(next == head) { /* go back to the head of the list, so there is a loop */

      next->next = curr;

      return TRUE;

    }

    Node *temp = curr;

    curr = next;

    next = next->next;

    curr->next = temp;

   }

   /* at the end of list, so there is no loop, let's reverse the list back */

   next = curr->next;

   curr ->next = NULL;

   while(next!=NULL) {

    Node *temp = curr;

    curr = next;

    next = next->next;

    curr->next = temp;

   }

   return FALSE;

  }

看上去这是一种奇怪的方法: 当有环的时候反转next指针会最终走到链表头部; 当没有环的时候反转next指针会破坏链表结构(使链表反向), 所以需要最后把链表再反向一次. 这种方法的空间复杂度是O(1), 实事上我们使用了3个额外指针;而时间复杂度是O(n), 我们最多2次遍历整个链表(当链表中没有环的时候).

这个方法的最大缺点是在多线程情况下不安全, 当多个线程都在读这个链表的时候, 检查环的线程会改变链表的状态, 虽然最后我们恢复了链表本身的结构, 但是不能保证其他线程能得到正确的结果.

3、 这是一般面试官所预期的答案: 快指针和慢指针

设置两个指针(fast, slow),初始值都指向头,slow每次前进一步,fast每次前进二步,如果链表存在环,则fast必定先进入环,而slow后进入环,两个指针必定相遇。(当然,fast先行头到尾部为NULL,则为无环链表)

int   is_link_list_cicled(Node*   head)    
    {    
            Node   *p   =   head,   *q   =   head;    
            while(p   &&   q)    
            {                        
                    p   =   p-> next;    
                    q   =   q-> next;    
                    if(!q)    
                            return   0;    
                    q   =   q-> next;    
<pre name="code" class="cpp">                    if(p   ==   q)    
                            return   1;    
 } return 0; }  

证明步长法的正确性(追击问题,如果有环则肯定可以相遇):

如果链表有环,不妨假设其环长度为M(>=2)。p指针首次到达交点(N0)时,q指针已经进入环。设p=0;q=q-p;再进过i(i>=0)步后,p=(p+i)%m;q=(q+2*i)%m;则必存在一个i使得(p+i)%m = (q+2*i)%m。(p,q 都为常数)。


二、找到环的入口点

当fast若与slow相遇时,slow肯定没有走遍历完链表,而fast已经在环内循环了n圈(1<=n)。假设slow走了s步,则fast走了2s步(fast步数还等于s 加上在环上多转的n圈),设环长为r,则:2s = s + nrs= nr设整个链表长L,入口环与相遇点距离为x,起点到环入口点的距离为a。a + x = nra + x = (n – 1)r +r = (n-1)r + L - aa = (n-1)r + (L – a – x)(L – a – x)为相遇点到环入口点的距离,由此可知,从链表头到环入口点等于(n-1)循环内环+相遇点到环入口点,于是我们从链表头、与相遇点分别设一个指针,每次各走一步,两个指针必定相遇,且相遇第一点为环入口点。程序描述如下

slist* FindLoopPort(slist *head)  
{  
    slist *slow = head, *fast = head;  
  
    while ( fast && fast->next )   
    {  
        slow = slow->next;  
        fast = fast->next->next;  
        if ( slow == fast ) break;  
    }  
  
    if (fast == NULL || fast->next == NULL)  
        return NULL;  
  
    slow = head;  
    while (slow != fast)  
    {  
         slow = slow->next;  
         fast = fast->next;  
    }  
  
    return slow;  
}  
扩展问题:判断两个单链表是否相交,如果相交,给出相交的第一个点(两个链表都不存在环)。
比较好的方法有两个:
一、将其中一个链表首尾相连,检测另外一个链表是否存在环,如果存在,则两个链表相交,而检测出来的依赖环入口即为相交的第一个点。
3,BITMAP

一、bitmap算法思想 

    32位机器上,一个整形,比如int a; 在内存中占32bit位,可以用对应的32bit位对应十进制的0-31个数,bitmap算法利用这种思想处理大量数据的排序与查询. 

    优点:1.运算效率高,不许进行比较和移位;2.占用内存少,比如N=10000000;只需占用内存为N/8=1250000Byte=1.25M。 
   缺点:所有的数据不能重复。即不可对重复的数据进行排序和查找。 

   比如: 
          第一个4就是 
          00000000000000000000000000010000 
          而输入2的时候 
          00000000000000000000000000010100 
          输入3时候 
          00000000000000000000000000011100 
          输入1的时候 
          00000000000000000000000000011110 

    思想比较简单,关键是十进制和二进制bit位需要一个map图,把十进制的数映射到bit位。下面详细说明这个map映射表。 

二、map映射表 

假设需要排序或者查找的总数N=10000000,那么我们需要申请内存空间的大小为int a[1 + N/32],其中:a[0]在内存中占32为可以对应十进制数0-31,依次类推: 
bitmap表为: 

a[0]--------->0-31 
a[1]--------->32-63 
a[2]--------->64-95 
a[3]--------->96-127 
.......... 

那么十进制数如何转换为对应的bit位,下面介绍用位移将十进制数转换为对应的bit位。 

三、位移转换 

例如十进制0,对应在a[0]所占的bit为中的第一位: 
00000000000000000000000000000001 

0-31:对应在a[0]中 

i =0                        00000000000000000000000000000000 
temp=0                  00000000000000000000000000000000 
answer=1                00000000000000000000000000000001 
i =1                         00000000000000000000000000000001 
temp=1                   00000000000000000000000000000001 
answer=2                 00000000000000000000000000000010 
i =2                          00000000000000000000000000000010 
temp=2                    00000000000000000000000000000010 
answer=4                  00000000000000000000000000000100 
i =30                         00000000000000000000000000011110 
temp=30                   00000000000000000000000000011110 
answer=1073741824  01000000000000000000000000000000 
i =31                         00000000000000000000000000011111 
temp=31                   00000000000000000000000000011111 
answer=-2147483648 10000000000000000000000000000000 

32-63:对应在a[1]中 

i =32                    00000000000000000000000000100000 
temp=0                00000000000000000000000000000000 
answer=1              00000000000000000000000000000001 
i =33                     00000000000000000000000000100001 
temp=1                 00000000000000000000000000000001 
answer=2               00000000000000000000000000000010 
i =34                      00000000000000000000000000100010 
temp=2                  00000000000000000000000000000010 
answer=4                00000000000000000000000000000100 
i =61                       00000000000000000000000000111101 
temp=29                  00000000000000000000000000011101 
answer=536870912   00100000000000000000000000000000 
i =62                        00000000000000000000000000111110 
temp=30                   00000000000000000000000000011110 
answer=1073741824  01000000000000000000000000000000 
i =63                         00000000000000000000000000111111 
temp=31                   00000000000000000000000000011111 
answer=-2147483648 10000000000000000000000000000000 


浅析上面的对应表: 
1.求十进制0-N对应在数组a中的下标: 
十进制0-31,对应在a[0]中,先由十进制数n转换为与32的余可转化为对应在数组a中的下标。比如n=24,那么 n/32=0,则24对应在数组a中的下标为0。又比如n=60,那么n/32=1,则60对应在数组a中的下标为1,同理可以计算0-N在数组a中的下标。 

2.求0-N对应0-31中的数: 
十进制0-31就对应0-31,而32-63则对应也是0-31,即给定一个数n可以通过模32求得对应0-31中的数。 

3.利用移位0-31使得对应32bit位为1. 
四、编程实现 

#include <stdio.h>

#define BITSPERWORD 32
#define SHIFT 5
#define MASK 0x1F
#define N 10000000

int a[1 + N/BITSPERWORD];//申请内存的大小

//set 设置所在的bit位为1
//clr 初始化所有的bit位为0
//test 测试所在的bit为是否为1

void set(int i) {        a[i>>SHIFT] |=  (1<<(i & MASK)); }
void clr(int i) {        a[i>>SHIFT] &= ~(1<<(i & MASK)); }
int  test(int i){ return a[i>>SHIFT] &   (1<<(i & MASK)); }

int main()
{	int i;
	for (i = 0; i < N; i++)
		clr(i);  
	while (scanf("%d", &i) != EOF)
		set(i);
	for (i = 0; i < N; i++)
		if (test(i))
			printf("%d\n", i);

	return 0;
}
解析本例中的void set(int i) {        a[i>>SHIFT] |=  (1<<(i & MASK)); } 

1.i>>SHIFT: 
其中SHIFT=5,即i右移5为,2^5=32,相当于i/32,即求出十进制i对应在数组a中的下标。比如i=20,通过i>>SHIFT=20>>5=0 可求得i=20的下标为0; 

2.i & MASK: 
其中MASK=0X1F,十六进制转化为十进制为31,二进制为0001 1111,i&(0001 1111)相当于保留i的后5位。 

比如i=23,二进制为:0001 0111,那么 
                         0001 0111 
                   &    0001 1111 = 0001 0111 十进制为:23 
比如i=83,二进制为:0000 0000 0101 0011,那么 
                          0000 0000 0101 0011 
                     &   0000 0000 0001 0000 = 0000 0000 0001 0011 十进制为:19 

i & MASK相当于i%32。 

3.1<<(i & MASK) 
相当于把1左移 (i & MASK)位。 
比如(i & MASK)=20,那么i<<20就相当于: 
         0000 0000 0000 0000 0000 0000 0000 0001 >>20 
      =0000 0000 0000 1000 0000 0000 0000 0000 

4.void set(int i) {        a[i>>SHIFT] |=  (1<<(i & MASK)); }等价于: 
void set(int i) 

   a[i/32] |= (1<<(i%32)); 
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Redisson是一个基于Redis的分布式和面向对象的Java库。它提供了许多分布式对象和服务,如分布式集合、分布式映射、分布式锁等,还支持许多特性,如发布/订阅、延迟队列等。而Redis是一个开源的内存数据存储,它被广泛用作缓存、消息队列和分布式锁等场景。 在面试中,Redisson可能会被问到的一些问包括: - Redisson的优势和特点是什么? - Redis和Redisson之间有什么区别? - Redisson的常见应用场景有哪些? - 在使用Redisson时需要注意哪些问? 注意,这只是一些可能被问到的面试目,具体的问可能会根据面试官的要求和公司的需求而有所不同。您可以根据自己的经验和知识来回答这些问,并举例说明。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *3* [46道史上最全Redis面试面试官能问的都被我找到了(含答案)](https://blog.csdn.net/m0_67698950/article/details/126179905)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] - *2* [Redis的87道高质量面试](https://blog.csdn.net/qq_43107323/article/details/104532857)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值