casyup.me@outlook.com

0%

前言

想深入了解一下(或者说验证)对象底层是如何工作的

重点是虚函数的调用, 虚指针的生成, 虚表中的数据

以及这些数据在多重继承, 虚继承, 多重虚继承环境下的表现

虚指针(virtual point)

考虑以下代码

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
class A{
public:
virtual void func(){
printf("A %d\n", _i);
}
virtual void func2(){
printf("A2 %d\n", _i);
}

int _i = 10;
};

class B : public A{
public:
virtual void func(){
printf("B %d\n", _i);
}

int _i = 20;
};

int main() {
B b;
A *pa = &b;

pa->func(); // B 20
pa->func2(); // A2 10

return 0;
}

输出和预期一致, 其中 func 是被 B 覆盖过的虚函数, 而 func2 则未被覆盖

以下是生成的汇编代码:

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
main:
.LFB1199:
pushq %rbp
movq %rsp, %rbp
subq $32, %rsp
movq $_ZTV1B+16, -32(%rbp) // 又是这个东西, 现在我怀疑它是 lippman 说的 thunk
// 后面知道了, **其实这就是虚指针, 它被放到了头部**
// 编译器非常聪明, 在即使有 vptr, nontrivial 的情况下
// 也并未生成构造函数
movl $10, -24(%rbp)
movl $20, -20(%rbp)
leaq -32(%rbp), %rax
movq %rax, -8(%rbp)
movq -8(%rbp), %rax
movq (%rax), %rax // rax = vptr 拿到虚指针地址
movq (%rax), %rax // rax = *vptr 获得虚指针中的地址(槽 0)
movq -8(%rbp), %rdx // member data 是直接位移得到的
movq %rdx, %rdi
call *%rax // 调用slot0
movq -8(%rbp), %rax
movq (%rax), %rax
addq $8, %rax // 虚指针地址 +8 位移(也就是下一个槽)
movq (%rax), %rax // 获得 slot1 的地址
movq -8(%rbp), %rdx
movq %rdx, %rdi
call *%rax // 调用slot1
movl $0, %eax
leave
ret

所以调用方式和书中一致, 从 vptr 索引虚表, 获得相应的 slot

这些信息全部都是在编译的时候由编译器生成

有一个点我忽略了: 单一继承对象只有一个虚表

后续我加上 C 对象后( C 继承自 A ), 让 B 继承 A, 这样A中就有 B C

但是依旧只有一个虚表, 仅当我让A再继承一个对象时, 这时产生了 2 个虚表

可能会问, 那么C对象是如何通过 pc->A::func() 这样的形式来调用 A 作用域的函数的呢?

答案是, 这会是一个单纯的函数调用, 并不会通过虚表或虚指针, 也没有任何的偏移

(emmm… 也就是说普通成员函数对于类来说, 可能更像是个陌生人, 即使它是成员)

(这也是为什么大多数情况下基类需要 virtual destruct 的原因)

虚析构是如何被调用的

在写标题的时候大概猜到了, 其实虚析构就是一个在子类中占了一个不能被重写的虚表槽

应该就是通过简单的偏移来调用的, 来试一下

(重点在于虚析构是不可能被覆盖的, 因为子类中不可能存在同名的函数)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
 5 class A{
6 public:
7 virtual ~A() {
8 printf("%s\n", "A destruct");
9 }
10 };
11
12 class B : public A{
13 public:
14 virtual ~B() {
15 printf("%s\n", "B destruct");
16 }
17 };
18
19 int main() {
20 B a;
21
22 return 0;
23 }

汇编:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
161 main:
162 .LFB1204:
163 .cfi_startproc
164 pushq %rbp
165 .cfi_def_cfa_offset 16
166 .cfi_offset 6, -16
167 movq %rsp, %rbp
168 .cfi_def_cfa_register 6
169 pushq %rbx
170 subq $24, %rsp
171 .cfi_offset 3, -24
172 movq $_ZTV1B+16, -32(%rbp)
173 movl $0, %ebx
174 leaq -32(%rbp), %rax
175 movq %rax, %rdi
176 call _ZN1BD1Ev // 就是这里调用了析构函数
177 movl %ebx, %eax
178 addq $24, %rsp
179 popq %rbx
180 popq %rbp
181 .cfi_def_cfa 7, 8
182 ret
183 .cfi_endproc

B的析构:

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
 89 _ZN1BD2Ev:	// 如果足够细心, 那么注意到了这里是 _ZN1BD2Ev 而并非 _ZN1BD1Ev
// 对此, 我发现编译器类似有个中间层一样的东西(或者说会符号替换?)
// 然后其实会跳转到这里来, 汇编中将这两个东西相关联
90 .LFB1201:
91 .cfi_startproc
92 .cfi_personality 0x3,__gxx_personality_v0
93 .cfi_lsda 0x3,.LLSDA1201
94 pushq %rbp
95 .cfi_def_cfa_offset 16
96 .cfi_offset 6, -16
97 movq %rsp, %rbp
98 .cfi_def_cfa_register 6
99 subq $16, %rsp
100 movq %rdi, -8(%rbp)
101 movq -8(%rbp), %rax
102 movq $_ZTV1B+16, (%rax)
103 movl $.LC1, %edi
104 call puts // 编译器知道只是打印一个字符串, 所以调用的是 puts
105 movq -8(%rbp), %rax
106 movq %rax, %rdi
107 call _ZN1AD2Ev // 和我想的不一样, 又一样...
108 movl $0, %eax
109 testl %eax, %eax
110 je .L6 // 这里我也不太懂, 这个不是必跳么?
111 movq -8(%rbp), %rax
112 movq %rax, %rdi
113 call _ZdlPv // 这个东西没有找到

和预料中不同的是, A 的析构函数是直接调用的, 而并非偏移

想了想这在意料之中, 因为编译器将它优化成了普通函数

至于 _ZdlPv, 没有在汇编中找到这个符号 …

(我试了一下, 没有什么好的方式让它像被虚函数一样调用, 很可惜…)

虚继承的析构函数调用

感觉非常奇怪, 所以本来是打算看看就好的, 因为之前花功夫去看了看虚继承的内存布局

不过实在太奇怪了, 所以打算好好分析一下, 顺便我在之前分析的时候好像忘了看虚继承的虚函数调用了

因为那时候好像已经晕了…

它会怎么被调用呢? 最无趣的情况就是像上面那样硬编码

不过在虚继承这种比较复杂的环境下, 编译器可能会做出其他反应也说不定?

源码就单纯的E被CD虚继承, B继承CD, 这里直接汇编:

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
main:
.LFB1212:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
pushq %rbx // 这里, 与前面的 pushq 无关
subq $56, %rsp
.cfi_offset 3, -24
leaq -64(%rbp), %rax // 前面有 pushq, 所以这里是 -64
movq %rax, %rdi
call _ZN1BC1Ev // 构造
movl $0, %ebx
leaq -64(%rbp), %rax
movq %rax, %rdi
call _ZN1BD1Ev // 析构
movl %ebx, %eax
addq $56, %rsp
popq %rbx
popq %rbp
.cfi_def_cfa 7, 8
ret
.cfi_endproc

B的析构函数:

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
457 _ZN1BD1Ev:
458 .LFB1210:
459 .cfi_startproc
460 .cfi_personality 0x3,__gxx_personality_v0
461 .cfi_lsda 0x3,.LLSDA1210
462 pushq %rbp
463 .cfi_def_cfa_offset 16
464 .cfi_offset 6, -16
465 movq %rsp, %rbp
466 .cfi_def_cfa_register 6
467 subq $16, %rsp
468 movq %rdi, -8(%rbp)
469 movl $_ZTV1B+24, %edx
470 movq -8(%rbp), %rax
471 movq %rdx, (%rax) // 栈顶加入了虚指针 +24
472 movl $32, %edx
473 movq -8(%rbp), %rax
474 addq %rax, %rdx
475 movl $_ZTV1B+104, %eax
476 movq %rax, (%rdx) // 栈顶 -32 处加入了虚指针 +104
477 movl $_ZTV1B+64, %edx
478 movq -8(%rbp), %rax
479 movq %rdx, 16(%rax) // 栈顶 -16 处加入了虚指针 +64
480 movq -8(%rbp), %rax
481 movl 28(%rax), %eax
482 movl %eax, %esi
483 movl $.LC3, %edi // .string "B destruct: %d\n"
484 movl $0, %eax
485 call printf
486 movl $_ZTT1B+24, %eax
487 movq -8(%rbp), %rdx
488 addq $16, %rdx
489 movq %rax, %rsi
490 movq %rdx, %rdi
491 call _ZN1CD2Ev // 硬编码...
492 movl $_ZTT1B+8, %edx
493 movq -8(%rbp), %rax
494 movq %rdx, %rsi
495 movq %rax, %rdi
496 call _ZN1AD2Ev
497 movl $2, %eax
498 testl %eax, %eax
499 je .L23
500 movq -8(%rbp), %rax
501 addq $32, %rax
502 movq %rax, %rdi
503 call _ZN1ED2Ev
504 nop

虚函数是通过硬编码来调用的

不过有个非常不错的发现, 我在 _ZTT1B 中发现了 LTHUNK 的影子

在 _ZTT1B 中有 LTHUNK 相关的代码, 不过很可惜, 这些猜测无从证明…

来看看虚函数的调用吧:

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
911 main:
912 .LFB1216:
913 .cfi_startproc
914 .cfi_personality 0x3,__gxx_personality_v0
915 .cfi_lsda 0x3,.LLSDA1216
916 pushq %rbp
917 .cfi_def_cfa_offset 16
918 .cfi_offset 6, -16
919 movq %rsp, %rbp
920 .cfi_def_cfa_register 6
921 pushq %rbx
922 subq $88, %rsp
923 .cfi_offset 3, -24
924 leaq -96(%rbp), %rax
925 movq %rax, %rdi
926 call _ZN1BC1Ev
927 leaq -96(%rbp), %rax
928 movq %rax, -24(%rbp)
929 movq -24(%rbp), %rax
930 movq (%rax), %rax // 解引用
931 addq $16, %rax // +16
932 movq (%rax), %rax // 取这个槽中的地址
933 movq -24(%rbp), %rdx
934 movq %rdx, %rdi
935 .LEHB0:
936 call *%rax
937 leaq -96(%rbp), %rax
938 movq %rax, -32(%rbp)
939 movq -32(%rbp), %rax
940 movq (%rax), %rax
941 addq $16, %rax // +16
942 movq (%rax), %rax
943 movq -32(%rbp), %rdx
944 movq %rdx, %rdi
945 call *%rax // A, B使用了一样的虚表和槽, 仅仅是this指针不同
946 leaq -96(%rbp), %rax
947 addq $16, %rax // +16
948 movq %rax, -40(%rbp)
949 movq -40(%rbp), %rax
950 movq (%rax), %rax
951 addq $16, %rax // +16
952 movq (%rax), %rax
953 movq -40(%rbp), %rdx
954 movq %rdx, %rdi
955 call *%rax // C 先是 +16 取到自己的虚表, 然后同样使用槽 1
956 .LEHE0:
957 movl $0, %ebx
958 leaq -96(%rbp), %rax
959 movq %rax, %rdi
960 call _ZN1BD1Ev
961 movl %ebx, %eax
962 jmp .L39
963 .L38:
964 movq %rax, %rbx
965 leaq -96(%rbp), %rax
966 movq %rax, %rdi
967 call _ZN1BD1Ev
968 movq %rbx, %rax
969 movq %rax, %rdi
970 .LEHB1:
971 call _Unwind_Resume
972 .LEHE1:
973 .L39:
974 addq $88, %rsp
975 popq %rbx
976 popq %rbp
977 .cfi_def_cfa 7, 8
978 ret
979 .cfi_endproc

普通虚函数的调用就是找到自己的虚表, 调用对应的槽

这个虚继承来的虚函数调用和普通继承的虚函数调用方式基本一致

多态是如何实现的呢?

每个构造函数, 都会覆写它的所有虚指针, 使他指向不同的地址(但是这些地址都不相同!!!)

如你所见, 当C指针去调用时, 发生了偏移, 虽然取到了同样的 slot, 但却不是同一张虚表

那么我能想到的就是有多张相同的虚表

(我只能如此猜测, 我想不到为什么这样的情况下还能调用同一个函数)

(但是为什么要有相同的呢? 这又是一个问题)

我尝试了看看这些虚表中是什么数据, 可惜失败了, 或者说看不懂

但是同一个类的虚表是完全相同的, 这可以肯定

不同的情况仅在于A类中C类和单独的C类, 这两个C类的虚表是不一样的

summary

虚函数机制和书中所说基本一致

  1. 找到对应的虚表
  2. 调用槽中的函数

review

我在 objdump 的输出中发现, 诸如 _ZTI1C 这样的东西在链接的时候会转化成一个数字常量

有些可能会是基于某个位置的偏移(比如 %rip)

编译器会利用它进行直接寻址, 有些会利用它进行间接寻址

前言

想要了解一下虚继承的内部数据结构

(语言: c++, 编译器: g++(4.8.5))

内存布局

考虑以下代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class A{
public:
int _i = 1;
};

class B : virtual public A{
public:
int _i = 2;
};

class C : virtual public A{
public:
int _i = 3;
};

class D : public B, public C{
public:
int _i = 4;
};

单纯的虚拟继承而已, 我使用了这种方式打印它们的内部数据

1
2
3
4
5
6
7
8
D a;
void *p = &a;
int *pi = (int *)p;

for (int i = 0; i < sizeof(a) / 4; ++i)
printf("%d\n", pi[i]);

printf("val:%d\n", a._i);

然后它们A, B, C, D内部数据情况如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
A: 	1
B: 4196632
0
2
1
C: 4196632
0
3
1
D: 4196760
0
2
0
4196784
0
3
4
1
0

emmm…

OK, 我可以把前两个 4 字节认为是虚指针(A没有)

那么A, B, C的内存分布就不用看了, 唯一比较特殊的是虚基类在内存的高地址

然后D的内部分布… 这… emmm… 仔细一想, 还好, 重要的是(Q1: 为什么最后补了一个0 ??)

存取方式

我试着访问了一下数据

1
2
3
4
5
6
7
8
9
D a;
A *pa = &a;
printf("%d\n", pa->_i);
B *pb = &a;
printf("%d\n", pb->_i);
C *pc = &a;
printf("%d\n", pc->_i);
D *pd = &a;
printf("%d\n", pd->_i);

来看一看汇编

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
pushq   %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
subq $80, %rsp // 80 栈帧
leaq -80(%rbp), %rax
movq %rax, %rdi
call _ZN1DC1Ev // 这里是构造函数, 稍后可能需要看看它的构造
leaq -80(%rbp), %rax
addq $32, %rax // +32, OK, 向上 32 字节的确是 1
movq %rax, -8(%rbp)
movq -8(%rbp), %rax
movl (%rax), %eax
movl %eax, %esi
movl $.LC0, %edi
movl $0, %eax
call printf // 1 第一次printf
leaq -80(%rbp), %rax
movq %rax, -16(%rbp)
movq -16(%rbp), %rax // B 类型指针并没有经过偏移, 这和预料的一样
movl 8(%rax), %eax // 这里+8, 跳过了虚指针
movl %eax, %esi
movl $.LC0, %edi
movl $0, %eax
call printf // 2 第二次printf
leaq -80(%rbp), %rax
addq $16, %rax // +16 C类型指针跳过了 B的内存, 这也没什么不对
movq %rax, -24(%rbp)
movq -24(%rbp), %rax
movl 8(%rax), %eax // 同理
movl %eax, %esi
movl $.LC0, %edi
movl $0, %eax
call printf // 3 第三次printf
leaq -80(%rbp), %rax
movq %rax, -32(%rbp)
movq -32(%rbp), %rax
movl 28(%rax), %eax // +28 OK~
movl %eax, %esi
movl $.LC0, %edi
movl $0, %eax
call printf // 4 第四次printf
movl $0, %eax
leave

数据的访问在编译时就已经定好的, 不存在额外效率影响

A指针 偏移了 +32 字节 直接访问数据

B指针 未偏移 访问数据时跳过了虚指针(8字节)

C指针 偏移 +16 字节 访问数据时跳过了虚指针(8字节)

D指针 未偏移 访问数据时跳过了虚指针 + B, C类数据(共28字节)

或许看一下访问虚基类的数据会有所了解?

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
pushq   %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
subq $80, %rsp
leaq -80(%rbp), %rax
movq %rax, %rdi
call _ZN1DC1Ev
leaq -80(%rbp), %rax
addq $32, %rax
movq %rax, -8(%rbp)
movq -8(%rbp), %rax
movl (%rax), %eax
movl %eax, %esi
movl $.LC0, %edi
movl $0, %eax
call printf // 基类访问方式和上面的汇编并没有不同
leaq -80(%rbp), %rax
movq %rax, -16(%rbp)
movq -16(%rbp), %rax
movq (%rax), %rax // 取指针指向的值, 也就是D的地址
subq $24, %rax // D地址减去24字节 也就是说, 它前面还有东西
movq (%rax), %rax // 获得那个地址中的值
movq %rax, %rdx
movq -16(%rbp), %rax
addq %rdx, %rax // 增加了 rdx 大小,
movl (%rax), %eax
movl %eax, %esi
movl $.LC0, %edi
movl $0, %eax
call printf
leaq -80(%rbp), %rax
addq $16, %rax
movq %rax, -24(%rbp)
movq -24(%rbp), %rax
movq (%rax), %rax
subq $24, %rax
movq (%rax), %rax
movq %rax, %rdx
movq -24(%rbp), %rax
addq %rdx, %rax
movl (%rax), %eax
movl %eax, %esi
movl $.LC0, %edi
movl $0, %eax
call printf
leaq -80(%rbp), %rax
movq %rax, -32(%rbp)
movq -32(%rbp), %rax
movq (%rax), %rax
subq $24, %rax
movq (%rax), %rax
movq %rax, %rdx
movq -32(%rbp), %rax
addq %rdx, %rax
movl (%rax), %eax
movl %eax, %esi
movl $.LC0, %edi
movl $0, %eax
call printf
movl $0, %eax
leave

emmm… 有一个重大的发现就是, 基类对象前面有数据

这些数据应该是偏移, 这些偏移配合 + 指针本身的地址能够访问到正确的虚基类数据

也就是说, 这是 深入探索对象 中, 对于虚基类的两种实现方式中的第一种

即: 在每个基类中添加虚基类的偏移, 但是又不全对…

越来越糊涂了 (눈_눈), 深吸一口气, 想想自己要干嘛… emmmm….

这样吧, 完整地看一遍构造的过程, 这样应该就明白了

(如果还是晕, 那么就希望下次遇到这个问题的时候能够更从容一些)

(我已经再这个问题上花太多时间, 再这样下去反而不好, 这也不是一个很重要/常用的知识)

完整的构造过程

首先是 main 区块

1
2
3
4
5
6
7
8
9
10
11
main:
.LFB1196:
pushq %rbp
movq %rsp, %rbp
subq $48, %rsp // 48 字节栈帧, 为什么是 48 呢?
leaq -48(%rbp), %rax
movq %rax, %rdi
call _ZN1DC1Ev // 调用构造函数
movl $0, %eax
leave
ret

然后是D的构造函数:

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
_ZN1DC1Ev:
.LFB1208:
pushq %rbp
movq %rsp, %rbp
subq $16, %rsp // 它再次开辟了 16 字节的栈帧
movq %rdi, -8(%rbp) // 把原栈顶指针放入了栈中
movq -8(%rbp), %rax
addq $32, %rax // 原栈顶指针 +32 偏移, 距栈底还有 16 字节
movq %rax, %rdi
call _ZN1AC2Ev // 这里留了 16 字节用于 A 的构造
movl $_ZTT1D+8, %edx // 我很好奇地查了 $_ZTT1D 这东西是多少, 然而事实让我绝望...
// 它有非常多的耦合, 短时间内我根本无法计算出这东西是多少!!!
// 而且里面的代码是以.开头的, 这意味着是伪指令... (;´༎ຶД༎ຶ`)
movq -8(%rbp), %rax
movq %rdx, %rsi // rdx 从未被赋值过, 我很好奇它将什么值给了 rsi
// 对了, 在上面一句用到了 edx, 这和 rdx 有关
// 暂且把它认为是一个, 编译器施加的魔法: magic
movq %rax, %rdi // 原栈顶指针
call _ZN1BC2Ev // 调用 B 构造函数
movl $_ZTT1D+16, %eax
movq -8(%rbp), %rdx // 拿出原栈顶指针
addq $16, %rdx // +16 偏移
movq %rax, %rsi
movq %rdx, %rdi
call _ZN1CC2Ev // 调用 C 构造函数
movl $_ZTV1D+24, %edxl
movq -8(%rbp), %rax
movq %rdx, (%rax) // 这里与调用 B 时的步骤重复了 :), 覆写
movl $_ZTV1D+48, %edx
movq -8(%rbp), %rax
movq %rdx, 16(%rax) // 这里与调用 C 时的步骤重复了 :), 覆写
movq -8(%rbp), %rax
movl $444, 28(%rax) // 写入值
leave
ret

A的构造函数:

1
2
3
4
5
6
7
8
9
10
11
_ZN1AC2Ev:
.LFB1199:
pushq %rbp
movq %rsp, %rbp
movq %rdi, -8(%rbp)
movq -8(%rbp), %rax
movl $111, (%rax) // 将 $111 放入了参数 rdi 指向的内存中 movl: 4字节
popq %rbp
ret // 嗯, 没了, 所以编译器仅仅对这 4 字节赋值
// 剩下的 12 字节呢?
// 其中8字节没用过, 或者不在 D 的内存中, 因为 sizeof d 是40, 而非48

B的构造函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
_ZN1BC2Ev:
.LFB1202:
pushq %rbp
movq %rsp, %rbp
movq %rdi, -8(%rbp) // 原栈顶指针
movq %rsi, -16(%rbp) // 未知的原 rdx(magic)
movq -16(%rbp), %rax
movq (%rax), %rdx // magic的地址 -16 中的值放入 rdx
movq -8(%rbp), %rax
movq %rdx, (%rax) // 将 rdx 中的值放入了栈顶, 这里是q, 占了 8 字节
movq -8(%rbp), %rax
movl $222, 8(%rax) // 栈顶 +8 偏移中, 放入了 $222
popq %rbp
ret // 共写入了 12 字节

C的构造函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
_ZN1CC2Ev:
.LFB1205:
pushq %rbp
movq %rsp, %rbp
movq %rdi, -8(%rbp)
movq %rsi, -16(%rbp)
movq -16(%rbp), %rax
movq (%rax), %rdx
movq -8(%rbp), %rax
movq %rdx, (%rax)
movq -8(%rbp), %rax
movl $333, 8(%rax)
popq %rbp // 步骤与 B 基本相同
ret

也就是说, 在D的构造中, ABC的构造都被调用了

要注意的一点是, BC都未调用A的构造, A的构造仅在D中调用了一次

同时要注意的是, BC会用那个编译器给的数字往自身的内存布局中写值

但是如果其上有D, D会将那个值覆写一次, 假设其值恒定不变, 位移分别为:

8 16 24 48

这是一组有规律的数字, 以 8 为开始, 每个数字是前面数字之和(这个就是偏移)

剩下来最重要的是, D覆写了什么?

再次来看看是如何访问虚基类数据的

1
2
3
D a;
C *pd = &a;
printf("%d\n", pd->A::_i);

汇编:

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
.LFB1196:
pushq %rbp
movq %rsp, %rbp
subq $48, %rsp
leaq -48(%rbp), %rax
movq %rax, %rdi
call _ZN1DC1Ev
leaq -48(%rbp), %rax
addq $16, %rax // 因为是 C 类型指针, 所以做了 +16 偏移
movq %rax, -8(%rbp) // C *pd = &a;
movq -8(%rbp), %rax
movq (%rax), %rax // 获得了指针中的值
subq $24, %rax // 该值减去 24
movq (%rax), %rax // 再以该值间接寻址
movq %rax, %rdx
movq -8(%rbp), %rax
addq %rdx, %rax // rdx 本身的值 + 指针的值
movl (%rax), %eax // 间接寻址得到的值就是虚基类的值
movl %eax, %esi
movl $.LC0, %edi
movl $0, %eax
call printf
movl $0, %eax
leave
ret

这次要清晰得多了, 我们以上面D的内存布局为例:

1
2
3
4
5
6
7
8
9
10
11
12
4196760	// 2. 它减去 24 到了这里 
// 3. 用这里的值寻址得到了某个数 x
0
2
0
4196784 // 1. 这是一开始 C 指针指向的内存
// 4. 用这个指针 + x 得到了虚基类数据的地址
0
3
4
1
0

那么在代码中试一下, 如果没有错误的话, 那个 x 应该是 4

(这个地址明显非常低, 它甚至没达到 8 字节, 这应该是编译的时候准备好的数据)

1
2
3
4
5
6
D a;
long long *pl = (long long *)&a;
long long *pl2 = (long long *)&pl;
*pl2 = *pl;

printf("%ld\n", *pl); // 16

取出来了 16, emmm… 嗯, 没错, 4 个 4字节 4 x 4 = 16 !!!!!!!

PS: 有没有思考过为什么通过双间接才能去到虚基类呢?

这里明显的一个问题是, 虚基类的数据是通过 C 的某个地址索引到 D 的某个地址

然后再用 C 的地址索引到虚基类数据, 有两次的跳转

我再次试了一下有继承三个虚基类的情况(就是D再继承了一个类似 BC的类)

发现它的访问方式是一样的, 也就是说, 每一个自身都会往前寻找固定的字节(编译后固定)

然后用那个地址的数据 + 指针本身的地址(我不知道我又没有表达清楚, 不过我感觉我没有 :) )

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
class E : virtual public A{
public:
int _i = 555;
};

class D : public B, public C, public E{
public:
int _i = 444;
};

int main() {
D a;
long long *pl = (long long *)&a;
long long *pl2 = (long long *)&pl;
*pl2 = *pl;
printf("%ld\n", *pl); // 32

pl = (long long *)&a + 2;
pl2 = (long long *)&pl;
*pl2 = *pl;
printf("%ld\n", *pl); // 16

pl = (long long *)&a + 4;
pl2 = (long long *)&pl;
*pl2 = *pl;
printf("%ld\n", *pl); // 0

内存布局:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
4196856	(32)// 2. 通过这个地址拿到偏移
0
222
0
4196880 (16)// 1. C对象, 当我要找虚基类对象时, 我先往上移动固定字节
// 这些移动的大小都是在编译时就写死了的常量(编译器自动计算)
// 往上移动的字节会随着数据大小而改变, 但是数据中的地址是不变的
// 比如我这里是4196880, 那么无论往上移动多少
// 上一个地址中存的值恒定为 当前值 - 24 (4196856)
// 3. 根据拿到的偏移 + 地址获取虚基类数据
0
333
0
4196904 (0)// E对象, 它如果想拿到虚基类数据, 执行和 C 对象一样的操作就好
0
555
444
111
0

这样的内存布局在其他方面可能还会有用

那么为什么会有 0 呢? 我估计是因为要实现这种内存布局, 或者说什么其他原因

summary

我很怀疑下次我看笔记时, 我自己看不看得懂我在说什么…

Intersection of two linked list

获得两条链表的连接点

my solution (36ms, 100%)

(虽然效率看起来是最好的, 但是其实比起最好的算法要差不少)

核心难点在于两条链表的长度不一定相等, 这样就不能遍历节点用等于的形式来判断

那么拿到两条链表的长度, 让较长的链表前进若干次, 以达到长度相等的情形, 那么就可以遍历了

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
class Solution {
public:
ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
int deep_A = 0, deep_B = 0;

ListNode *temp_head = headA;
while (temp_head) {
temp_head = temp_head->next;
++deep_A;
}

temp_head = headB;
while (temp_head) {
temp_head = temp_head->next;
++deep_B;
}

int distance = abs(deep_A - deep_B);
if (deep_A > deep_B) {
while (distance) {
headA = headA->next;
--distance;
}
} else {
while (distance) {
headB = headB->next;
--distance;
}
}

while (headA && headB && headA != headB) {
headA = headA->next;
headB = headB->next;
}

return headA;
}
};

代码如下, 会产生 3 次遍历, 两次完整的遍历 O(3n) => O(n)

the best solution

1
2
3
4
5
6
7
8
9
10
11
12
class Solution {
public:
ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
ListNode *p = headA;
ListNode *q = headB;
while (p != q) {
p = (p ? p->next: headB);
q = (q ? q->next: headA);
}
return p;
}
};

原来还可以这么做!!!

算法的核心思想在于, A + B = B + A

假设有两条链表, 长度不等

0 0 0 2 1 1    // A
0 0 2 1 1    // B

其中将他们的链接点标为 2

那么有以下组合链表

0 0 0 2 1 1 0 0 2 1 1    // A + B
0 0 2 1 1 0 0 0 2 1 1    // B + A

他们如果有链接点, 那么在第二次循环时必然相等

我是用求深度差来解决这个问题, 这个算法更加巧妙地用组合链表的形式规避了这个深度差

152 Maximum Product Subarray

找到数组中最大的连续乘积

my solution (8ms, 100%)

先将乘积以 0 为分界线分开, 这样得到的就是一段段最大的乘积(暂时无视正负)

而如果得到的数是负数, 那么分为 4 种情况

  1. 从左到右, 找到第一个负数, 用当前乘积除以它
  2. 从左到右, 找到第一个负数, 以该负数为界限, 获取左边的乘积
  3. 从右到左, 找到第一个负数, 用当前乘积除以它
  4. 从右到左,, 找到第一个负数, 以该负数为界限, 获取右边的乘积

这四种情况最大者, 视为这一段的最大值, 而所有这些段再做比较, 就得到了整个数组连续乘积的最大值

代码如下:

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
static const int _ = [] () {
ios::sync_with_stdio(false);
cin.tie(nullptr);

return 0;
} ();

class Solution {
public:
int maxProduct(vector<int>& nums) {
size_t size = nums.size();
if ( !size ) return 0;

int b = 0;
int e = size;
int ret = nums[0];
while (b < e) {
int tb = b;
while (b < e && nums[b] != 0) ++b;
if (b - tb <= 1) {
ret = max(ret, nums[tb]);
if (b < e)
ret = max(ret, nums[b]);
b += 1;
continue;
}

int sum = 1;
int tb2 = tb;
while (tb2 < b) sum *= nums[tb2++];

if (sum < 0) {
int sumneg1 = 1;
tb2 = tb;
while (tb2 < b) {
sumneg1 *= nums[tb2];
if (nums[tb2] < 0) break;
++tb2;
}
int sum1 = sumneg1 / nums[tb2] == 1 ? sumneg1 :
sumneg1 / nums[tb2];

int sumneg2 = 1;
tb2 = b - 1;
while (tb2 >= tb) {
sumneg2 *= nums[tb2];
if (nums[tb2] < 0) break;
--tb2;
}
int sum2 = sumneg2 / nums[tb2] == 1 ? sumneg2 :
sumneg2 / nums[tb2];

sum /= max(sumneg1, sumneg2);
sum = max(sum, sum1);
sum = max(sum, sum2);
}

ret = max(ret, sum);
b += 1;
}

return ret;
}
};

不太满意, 代码有点过长了…

best solution

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Solution {
public:
int maxProduct(vector<int>& nums) {
int len = nums.size();
int max_num[len] = {0};
int min_num[len] = {0};
int max = nums[0];
int min = nums[0];
max_num[0] = nums[0];
min_num[0] = nums[0];
for (int i = 1; i < len; ++i) {
int tmp1 = max_num[i-1] * nums[i];
int tmp2 = min_num[i-1] * nums[i];
int cur_max_num = tmp1 > tmp2 ? tmp1 : tmp2;
int cur_min_num = tmp1 < tmp2 ? tmp1 : tmp2;
max_num[i] = cur_max_num >= nums[i] ? cur_max_num : nums[i];
min_num[i] = cur_min_num <= nums[i] ? cur_min_num : nums[i];
max = max > max_num[i] ? max : max_num[i];
min = min < min_num[i] ? min : min_num[i];
}

return max;
}
};

啊. 简洁的小东西

不过, 我不是太懂这个代码的含义…

不过我觉得我的想法会好点, 虽然代码实现起来有点困难

但是它能一次性跳过很大一片区域, 对于连续整数很多的情况, 能够很快地计算出来

而这种的话会完完整整的遍历一次

emmm. 是的, 我觉得我的想法会好一点 :)

Candy

按照规则给每个小孩发糖果

  1. 每个小孩的糖果数至少为 1
  2. 每个小孩获得的糖果数比他临近的, 并且权重比他小的小孩获得的要多

my solution(12ms, 100.00%)

虽然做出来了, 但是并不高兴 = =, 因为代码太过复杂

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
93
94
95
96
97
98
99
100
const static int _ = [] () {
ios::sync_with_stdio(false);
cin.tie(nullptr);

return 0;
} ();

class Solution {
public:
int candy(vector<int>& ratings) {
size_t size = ratings.size();
if (!size) return 0;
if (size == 1) return 1;

vector<int> candys(size, 0);

// 填充所有可以直接确定的数
if (ratings[0] <= ratings[1])
candys[0] = 1;
if (ratings[size - 1] <= ratings[size - 2])
candys[size - 1] = 1;
for (int i = 1; i + 1 < size; ++i) {
int minn = min(ratings[i], ratings[i - 1]);
minn = min(minn, ratings[i + 1]);

if (ratings[i] == minn)
candys[i] = 1;
}

// 正向遍历
if (ratings[0] > ratings[1] && candys[1])
candys[0] = candys[1] + 1;
if (ratings[size - 1] > ratings[size - 2] && candys[size - 2])
candys[size - 1] = candys[size - 2] + 1;
for (int i = 1; i + 1 < size; ++i) {
if (candys[i] == 0) {
int minn, maxn;
// 当他是临近的人中权重最大的人时, 他的糖果数是
// max(left, right) + 1
if (ratings[i + 1] == ratings[i - 1]) {
if (candys[i + 1] && candys[i - 1])
candys[i] = max(candys[i + 1], candys[i - 1]) + 1;
continue;
}
else if (ratings[i + 1] > ratings[i - 1]) {
minn = i - 1;
maxn = i + 1;
}
else {
minn = i + 1;
maxn = i - 1;
}

// 当他只比其中一个大时, 他的糖果数是 min + 1
if (ratings[i] <= ratings[maxn] && candys[minn])
candys[i] = candys[minn] + 1;
// 这里和第一处的判断是相同的, 第一处只是为了减少一些操作
if (ratings[i] > ratings[maxn] && candys[maxn] && candys[minn])
candys[i] = max(candys[maxn], candys[minn]) + 1;
}
}

// 反向遍历, 逻辑相同
for (int i = size - 2; i >= 1; --i) {
if (candys[i] == 0) {
int minn, maxn;
if (ratings[i + 1] == ratings[i - 1]) {
if (candys[i + 1] && candys[i - 1])
candys[i] = max(candys[i + 1], candys[i - 1]) + 1;
continue;
}
else if (ratings[i + 1] > ratings[i - 1]) {
minn = i - 1;
maxn = i + 1;
}
else {
minn = i + 1;
maxn = i - 1;
}

if (ratings[i] <= ratings[maxn] && candys[minn])
candys[i] = candys[minn] + 1;
if (ratings[i] > ratings[maxn] && candys[maxn] && candys[minn])
candys[i] = max(candys[maxn], candys[minn]) + 1;
}
}
if (ratings[0] > ratings[1] && candys[1])
candys[0] = candys[1] + 1;
if (ratings[size - 1] > ratings[size - 2] && candys[size - 2])
candys[size - 1] = candys[size - 2] + 1;

int ret = 0;
for (int i = 0; i < size; ++i)
ret += candys[i];

return ret;
}

private:
};

整体代码会有 3 次遍历, 但是代码太多, 太复杂了…

如果没有看到最好算法的简洁程度, 我可能还能再高兴一下…

best solution

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
class Solution {
public:
int candy(vector<int>& ratings) {
vector<int> num(ratings.size(), 1);
for (int i = 1; i < ratings.size(); ++i) {
if (ratings[i] > ratings[i-1]) {
num[i] = num[i-1] + 1;
}
}
for (int i = ratings.size() - 2; i >= 0; --i) {
if (ratings[i] > ratings[i+1] && num[i] <= num[i+1]) {
num[i] = num[i+1] + 1;
}
}
int sum = 0;
for (int i = 0; i < num.size(); ++i) {
sum += num[i];
}
return sum;
}
};

static int x = []() {
ios::sync_with_stdio(false);
cin.tie(NULL);
cout.tie(NULL);
return 0;
}();

(做完题的喜悦被冲刷得干干净净…, 啊… 真的是羞愧难当啊…)

(我为什么总是把问题复杂化了呢…)

逻辑上大致相同, 只不过他只需要一次 反向遍历, 一次 正向遍历

其中每次遍历所需要判断的条件都十分简单并且清晰

3Sum Closest

给一个包含多个整数的数组 nums, 以及一个整数 target, 在 nums 中找一个三整数, 满足和最接近 target

返回这个和, 你可以假定每一个输入都只有一个解法

my code

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
class Solution {
public:
int threeSumClosest(vector<int>& nums, int target) {
// nums.size() return value is size_t !!!!
if (nums.size() < 3) return 0;
if (nums.size() == 3) return nums[0] + nums[1] + nums[2];
cut(nums);
int length = nums.size() - 1;
minn = nums[0] + nums[1] + nums[2];
targetp = target;

for (int i = 0; i <= length; ++i) {
for (int i2 = 0; i2 <= length; ++i2) {
if (i2 != i) {
for (int i3 = 0; i3 <= length; ++i3) {
if (i3 != i2 && i3 != i) {
if ( !isMin(nums[i] + nums[i2] + nums[i3]) )
break;
} // 3 if
else continue;
} // 3
} // 2 if
else continue;
} // 2
} // 1

return minn;
}

private:
void cut(vector<int>& nums) {
sort(nums.begin(), nums.end());
}

bool isMin(int newn) {
minn = abs(newn - targetp) > abs(targetp - minn) ? minn : newn;

return newn < targetp;
}

int minn;
int targetp;
};

非常愚蠢地选择了丑陋的forforfor…

比较主要的是重新排序了集合, 这样在循环中的值就会越来越大,

那么当超过一定数值的时候, 就可以break出去, 减少更多的无用计算

不过依旧差不多是 o3 的复杂度

memery and speed

速度在统计图之外, 内存使用倒是挺少的 (算法就是要空间换时间啊, MU少有什么用 = =)

dalao‘s code

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
class Solution {
public:
int threeSumClosest(vector<int>& nums, int target) {
int n = nums.size();
if (n < 3) return 0;
int rnum = nums[0] + nums[1] + nums[2];
sort(nums.begin(), nums.end());

for (int i = 0; i < (n - 2); ++i) {
int lb = i + 1, ub = n - 1;
while (lb < ub) {
int tmpsum = nums[i] + nums[lb] + nums[ub];
if (tmpsum == target) return target;
if (tmpsum > target) {
if ((tmpsum - target) < abs(rnum - target)) rnum = tmpsum;
--ub;
} else {
if ((target - tmpsum) < abs(rnum - target)) rnum = tmpsum;
++lb;
}

}
}
return rnum;
}
};

他的思路也是排序后, 根据有顺序的元素进行优化

不过是按照首位逼近的方式来优化, 并且没有丑陋的forforfor (diediedie!!!)

代码少了很多, 但是也更加简洁和清晰了

Letter combinations of a phone number

给一个包含 2-9 整数的字符串, 返回所有可能的数字能代表的字母组合

my code

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
class Solution {
public:
vector<string> letterCombinations(string digits) {
if (digits.empty()) return {};

doit(digits, {});

return ret;
}

private:
void doit(string digits, string const& str) {
if (digits.size() == 0) {
ret.emplace_back(str);
return;
}

vector<char>& charset = dict[int(digits[0]) - 48];

for (int i = 0; i < charset.size(); ++i) {
doit({digits.begin() + 1, digits.end()}, str + charset[i]);
}
}

map<int, vector<char>> dict = {
{2, {'a', 'b', 'c'}},
{3, {'d', 'e', 'f'}},
{4, {'g', 'h', 'i'}},
{5, {'j', 'k', 'l'}},
{6, {'m', 'n', 'o'}},
{7, {'p', 'q', 'r', 's'}},
{8, {'t', 'u', 'v'}},
{9, {'w', 'x', 'y', 'z'}}
};

vector<string> ret;
};

简单地存储一个映射, 然后循环调用, 每次获取一个字符

因为循环深度不可控, 以及为了简单易懂, 所以选择了递归, 不过这会带来额外的栈帧

内存开销上会多不少, 速度上差别倒是不大

dalao’s code

简单看了一下, 是用 for 循环实现的

因为时间上并没有什么差距, 原理也很好懂, 这次就不具体贴出来了

反转二进制

my solution

我选的方法是直接调用 bits.Reverse 函数来解决, 如果自己写的话, 应该会循环获取每一位, 重新构造一个数字

1
2
3
func reverseBits(num uint32) uint32 {
return bits.Reverse32(num)
}

best solution

这也是为什么一道 easy 的题会单独做笔记的原因, 因为这道题的解法很有意思

1
2
3
4
5
6
7
8
func reverseBits(num uint32) uint32 {
constNums := []uint32{0xAAAAAAAA, 0x55555555, 0xCCCCCCCC, 0x33333333, 0xF0F0F0F0, 0x0F0F0F0F, 0xFF00FF00, 0x00FF00FF, 0xFFFF0000, 0x0000FFFF}
var index, power uint32
for index, power = 0, 0; index <= 8; index, power = index+2, power+1 {
num = ((num & constNums[index]) >> (1 << power)) | ((num & constNums[index+1]) << (1 << power))
}
return num
}

乍看之下理解不了, 特别是循环中的那句代码, 先给个例子来展示一下 :

原二进制:       00000010100101000001111010011100
第一次循环后:       00000001011010000010110101101100
第二次循环后:       00000100100100101000011110010011
第三次循环后:       01000000001010010111100000111001
第四次循环后:       00101001010000000011100101111000
第五次循环后:       00111001011110000010100101000000
1
((num & constNums[index]) >> (1 << power)) | ((num & constNums[index+1]) << (1 << power))

代码的含义是这样的(诶… 我想想怎么解释一下 = =)

emmm, 先看一个最简单的例子, 那么就能理解了, 首先, 取二进制 01, 只执行一次循环, 看看效果

num & constNums[index] = 0, (num & constNums[index+1]) = 1, power = 0, 所以结果是 10, 我们得到了反转字符串

那么再来难点的例子, 1101, 这样需要执行两次循环

num & constNums[index] = 1000, (num & constNums[index+1]) = 0101, power = 0, 所以第一次循环的结果是 0100 | 1010 = 1110

num & constNums[index] = 1100, (num & constNums[index+1]) = 0010, power = 1, 所以结果是 0011 | 1000 = 1011

这样大概就能了解了吧, 第一次循环是两位为一组交换位置, 第二次循环是四位为一组交换位置, 所以第一次的二进制是 0xAAAAAAAA, 0x55555555 类似与 101010… 010101… 这样的, 他们的二进制位是相反的, 同理直到达到足够的位数为止, 这道题的复杂度是 O(logn) 级别的, 其中 s = 数字的位数, s = 2 的 n 次方

真是个不错的算法 :)

这个算法能见到 二分 和 递归 的影子, 理解了就会非常简单

看了 go 对于该算法的实现, 也是这样的

顺便, 看了 go 的源码, 其实句子可以更简单

1
constNums[index] & num >> (1 << power) | constNums[index+1] & num << (1 << power)

整个代码也可以重构

1
2
3
4
5
6
7
8
9
10
11
12
func reverseBits(n uint32) uint32 {
var m0 uint32 = 0x55555555
var m1 uint32 = 0x33333333
var m2 uint32 = 0x0F0F0F0F
var m3 uint32 = 0x00FF00FF

n = n >> 1 & m0 | n & m0 << 1
n = n >> 2 & m1 | n & m1 << 2
n = n >> 4 & m2 | n & m2 << 4
n = n >> 8 & m3 | n & m3 << 8
return n >> 16 | n << 16
}

这基本与 go 的 reverse32 源码一致, 不愧是源码啊…

这个版本的代码进一步减少了变量和常量, 其中那 index 所代表的一对本就可以用移位来代替

Minimun Size Subarray Sum

不想解释…

my solution

简单思考了一下, 以每个元素为起点, 循环往右找到满足条件的数

最终做比较, 就可以得出答案

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
func minSubArrayLen(s int, nums []int) int {
size := len(nums)
ret := size
var sum int = 0

for _, v := range(nums) {
sum += v
}
if (sum < s) {
return 0
}

for i, v := range(nums) {
sum = v
counter := 1
for i2 := i + 1; i2 < size && sum < s; i2++ {
sum += nums[i2]
counter++
}

if sum >= s && counter < ret {
ret = counter
}
}

return ret
}

这样的复杂度大概是 O(n2), 同时为了区别特殊情况, 还做了一次…

the best solution

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
func minSubArrayLen(s int, nums []int) int {
size := len(nums)
ret := size + 1
start := 0
var sum int = 0

for i, v := range(nums) {
sum += v

for sum >= s && sum - nums[start] >= s {
sum -= nums[start]
start++
}

if sum >= s && i - start + 1 < ret {
ret = i - start + 1
}
}

if ret == size + 1 {
return 0
}

return ret
}

其实这是我将算法思想重写后的代码…

简单来说, 就是先找到匹配的 sum, 然后没加一次, 就尝试减少前面的数

这样遍历之后, 得到的数同样也是最短的

(我很好奇没有是否没有人对数级的解决方案…)

222. Count Complete Tree Nodes

统计 complete tree 节点数量

my solution

首先说说我的做法, 我首先想到的是用栈来保存其数据结构, 跟踪路径, 从右至左, 直到找到一条能到达底层的路

first. my idea is use stack to save the data structure, trace the route, from right to left, until find a way to bottom node

(前些天被一个 13 说我 leetcode 从不写注释, 我怨念到了现在 = =)

(I was told by a 13 the other day that i never write comments in leetcode. I remember it till now :( )

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
class Solution {
public:
int countNodes(TreeNode* root) {
stack<pair<TreeNode*, bool>> nodes;

TreeNode* cursor = root;
int ldeep = 0, rdeep = 0;
while (cursor) {
cursor = cursor->left;
++ldeep;
}
cursor = root;
while (cursor) {
// 1 means next is a right node
nodes.push(make_pair(cursor, 1));
cursor = cursor->right;
++rdeep;
}
int ret = (1 << ldeep) - 1;
// is a full binary tree
if (ldeep == rdeep)
return ret;

while (!nodes.empty()) {
cursor = nodes.top().first;
if (!cursor->right) --ret;
if (!cursor->left) --ret;
if (cursor->left || cursor->right)
return ret;

nodes.pop();
--rdeep;
// current node is right node, so next node is left node
if (!nodes.empty() && nodes.top().second) {
nodes.top().second = 0;
nodes.push(make_pair(nodes.top().first->left, 1));
++rdeep;
continue;
}

// find next node with next node is right node
while (!nodes.empty() && !nodes.top().second) {
nodes.pop();
--rdeep;
}
// means some errors occur
if (nodes.empty()) return -1;
nodes.top().second = 0;
cursor = nodes.top().first->left;
while (cursor) {
nodes.push(make_pair(cursor, 1));
cursor = cursor->right;
++rdeep;
}
// get the bottom element
if (ldeep == rdeep) return ret;
}

// means some errors occur
return -2;
}
};

最不理想的情况下, 最左边只有一个节点, 会遍历掉所有节点 = =. stack 容量应该不会超过 31 个元素.

代码不够整洁, 不够易懂. 勉强算个解法.

best solution

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
inline bool is_high (TreeNode* root, int path, int high) {
high--;
while (high >= 0) {
bool flag = path & (1 << high);
if (flag && root->right) {
root = root->right;
} else if (!flag && root->left) {
root = root->left;
} else {
return false;
}
high--;
}

return true;
}

class Solution {
public:
int countNodes (TreeNode* root) {
if (!root) return 0;

int high = 0;
TreeNode* node = root->left;
while (node) {
high++;
node = node->left;
}

int first = 0;
int last = 1 << high;
while (first < last) {
int mid = (first + last) / 2;
if (is_high(root, mid, high)) {
first = mid + 1;
} else {
last = mid;
}
}

return (1 << high) - 1 + last;
}
};

最后的 return 里面, 用 first 和 last 都可以, 我觉得 last 语义可能更好, 所以换成了 last.

on the end of return, use both first and last. i think ‘last’ more make sence, so i use ‘last’

他的计算是 full tree 节点数 + 底层节点数. 从这个算法中我学到了一些东西, 二进制和 binary tree, 真的有很大可操作空间.

the algorithm is a number of full tree nodes plus bottom nodes. I study something from the algorithm. it’s a lot of skill can use between binary system and binary tree.

在分析算法前, 我先总结一下我从这个算法中看到的规律.

before profiling this algorithm, summarize some rules of complete tree that I saw in this algorithm

首先, 将树节点排序, root 为 1, 从左到右, 从上到下, 依次递进. 得到类似的图 :

first, sort tree nodes. root is number 1, from left to right, up to down, get this map:

                            1
            2                                3
    4                5                6                7
8(1)    9(2)    10(3)    11(4)    12(5)    13(6)    14(7)    15(8)

假设一个数, 5, 其二进制为 101, 已知是 binary tree, 所以每个节点的 2 倍就是当前数字右移 1 位, 右尾置 0. 所以 0 代表向左下前进, 1 则代表向右下前进. 分为两种情况.

assume a number, for example 5, it’s binary expression is 101.

  1. 将首位看做无效位, 则 101 = 01, 代表从根向左->向右, 则得到了数字 5.

    treat the first digit as an invalid bit. 101 means left -> right. get the number 5.

  2. 将首位看做有效位, 则 101 = 101, 代表从根向右->向左->向右, 得到数字 13, 而这个数字, 刚好是底部从左往右的第 5 个数字 + 1 = 第 6 个数字.

    treat the first digit as an valid bit. 101 means right -> left -> right. get the number 13. the number sorted by bottom level is the the original number plus one.

    (为什么呢? 为什么刚好是 + 1? 因为恒定的, 因为这是 complete tree, 每一层到下一层的相对应那个数的步数都是恒定的. 都是 (2 << n) - 1, 而 + 1 就刚好是 2 << n, 这和刚才将首位看做有效位对应上了, 其实将首位看做有效位也就是增加了 2 << n 而已.)

将一些规律总结后, 就变简单了许多

after summarize some rules, things more simple.

1
2
3
4
5
6
7
8
while (first < last) {
int mid = (first + last) / 2;
if (is_high(root, mid, high)) {
first = mid + 1;
} else {
last = mid;
}
}

这段代码就是在首位间不断逼近, 求得的最大的数字, 也就是最后一排.

this code just get more close between ‘first’ and ‘last’. get biggest number, also is the result.

is_high(root, mid, high) 实则是在判断, 最后一排有没有 mid + 1 这个数字, 如果有, 则 first = mid + 1, 意味着这个数字是存在的, 继续不断逼近. 直到最后 last == first, 因为 first = mid + 1, 而 mid = (first + last) / 2, 所以 first 最大不会超过 last.

‘is_higt’ judge is there a number ‘mid + 1’. if is, then ‘first = mid + 1’. means this number is exist.

所以这个解法是利用了 complete tree 的特性. 相当简洁, 也没有使用额外的空间, 时间复杂度是 O(logn), 很不错的解法.

I’m tired… :(

thanks for browsing :)