查找中点

有这么一道面试题:

找出未知单链表中点元素。

市面上有2种方法:

  1. 先遍历算出总长度为length,再遍历length/2个元素。
  2. 使用快慢指针。快指针步长为2,慢指针为1。快指针到达链表末端时,慢指针正好为中点元素。

然而这2种方法本质上没有差别。

###废话不说先上结果:

0.00027776 s : find_mid 循环法
0.000276798 s : find_mid_1 双指针
2.81981 s : find_mid 提高指针取得下一个元素的代价
2.81391 s : find_mid_1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
#include <iostream>
#include <thread>
#include <sys/timeb.h>
#include <time.h>
#include <windows.h>
using namespace std;
class list {
private:
list *next = nullptr;

public:
int m_d;
list* add(list* p) { next = p; return p; }
list* get_next() { Sleep(delay); return next; }
static int delay;
};
int list::delay = 0;
list* find_mid(list* ptr)
{
int len = 0;
list *p_start = ptr;
while (true)
{
if (!p_start)
break;
len++;
p_start = p_start->get_next();
}
len /= 2; p_start = ptr;
while (len--)
{
p_start = p_start->get_next();
}
return p_start;
}
list* find_mid_1(list* ptr)
{
list *p_s1 = ptr, *p_s2 = ptr;
while (p_s2=p_s2->get_next())
{
p_s2 = p_s2->get_next();
if (!p_s2)
break;
p_s1 = p_s1->get_next();
}
return p_s1;
}
#define cal_time(fun,ptr) {\
QueryPerformanceCounter(&startCount);\
fun(ptr);\
QueryPerformanceCounter(&endCount);\
double elapsed = (double)(endCount.QuadPart - startCount.QuadPart) / freq.QuadPart;\
cout << elapsed<<" s : "<< #fun << endl;}

int main()
{
int c = 1*1000;
list * start = new list,*p;
p = start;
while (c--)
{
p = p->add(new list);
p->m_d = c;
}
LARGE_INTEGER startCount;
LARGE_INTEGER endCount;
LARGE_INTEGER freq;
QueryPerformanceFrequency(&freq);
_ASSERT(find_mid_1(start) == find_mid(start));
cal_time(find_mid, start);
cal_time(find_mid_1, start);
list::delay = 1;

cal_time(find_mid, start);
cal_time(find_mid_1, start);
return 0;
}

#分析
其实这2种方法都是O(n),使用大O表示法不够精确,这种简单的算法直接数一下next的调用次数就可以知道都是1.5*len个get_next().find_mid_1 唯一的优势就是少用了一个 int len。所以性能提升实在少的可怜。

#下面给出正确的方案
思路:其实也是用到了2个指针,其中一个走过的路程是另一个的1/2。但千万不要使用一个+1另一个+2。而是在前一个指针遍历的时候保存下来给另一个指针。

##原理:
虽然我们不能预知链表的长度,但是我们可以预期

前一个指针移动了n步时,将这个值赋值给后一个指针,然后预期前一个指针能访问到2n,如果预期达成,那么继续开展预期。如果预期失败,那么中点为上一次预期成功的点再移动 c/2步。

##分析:
上一次预期成功的点就是节约计算的关键。

最好情况为:预期成功了,然后链表也到底了。

最差情况为:链表长度为2^n-1。上一次预期成功的点为(2^(n-2))然后还要再走(2^(n-2))步。

当然如果增加期望点,性能还有望提升。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
list* find_mid_2(list* ptr)
{
list *p_s1 = ptr, *p_s2 = ptr, *tmp= ptr;
int d = 1,t=1;
int i = 0;
while (true)
{

tmp = p_s2;
for (int c=0; i < d; i++,c++)
{
p_s2 = p_s2->get_next();
if (!p_s2)
{
int len = c / 2;
while (len--)
{
p_s1 = p_s1->get_next();
}
return p_s1;
}
}
d *= 2;
p_s1 = tmp;
}
return p_s1;
}

来看效果吧

最好情况
0.608294 s : find_mid
0.612391 s : find_mid_1
0.404288 s : find_mid_2

最差情况
0.604105 s : find_mid
0.600299 s : find_mid_1
0.502341 s : find_mid_2

我估算需要(1,1.25)n个get_next()操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
#include <iostream>
#include <thread>
#include <sys/timeb.h>
#include <time.h>
#include <windows.h>
#include <list>
#include <queue>
#include <map>
using namespace std;
typedef list<int> mlist;
#define cal_time(fun,ptr) {\
QueryPerformanceCounter(&startCount);\
fun(ptr);\
QueryPerformanceCounter(&endCount);\
double elapsed = (double)(endCount.QuadPart - startCount.QuadPart) / freq.QuadPart;\
cout << elapsed<<" s : "<< #fun << endl;}

class stage {
public:
mlist* msrc;
map<int, mlist::iterator, std::greater<int> > m_map;
stage(mlist& m)
{
msrc = &m;
}
mlist::iterator operator[](int n)
{
if (n == 0)
return mlist::iterator();
map<int, mlist::iterator, std::greater<int> >::iterator itr = m_map.find(n);

if (itr != m_map.end())
{
return itr->second;
}
else
return operator[](n/2);
}
};

mlist::iterator find_mid_2(mlist ptr) {
stage probe_ptr(ptr);
auto& m_map = probe_ptr.m_map;
auto itr = ptr.begin();
m_map[1] = itr;
int len = 1;
while (itr!=ptr.end())
{
auto m_itr = m_map.begin();
int top_key=m_itr->first;
while (m_itr!=m_map.end())
{
if(m_itr->first<top_key/2)
break;
if(len==2*top_key)
m_map[len]=itr;
m_itr++;
}

itr++; len++;
}
for (auto effect:m_map)
{
int k = effect.first;
if (m_map.find(k) != m_map.end())
{
k = k / 2;
auto mid = m_map[k];
while (k++<len/2)
{
mid++;
}
cout << "res:" << *mid << endl;
return mid;
}
}
}
int main()
{
int c = 1 * 10;
mlist start;
while (c--)
{
start.push_back(c);
}
LARGE_INTEGER startCount;
LARGE_INTEGER endCount;
LARGE_INTEGER freq;
QueryPerformanceFrequency(&freq);

cal_time(find_mid_2, start);
return 0;
}

坚持原创技术分享,您的支持是我前进的动力!