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));
}