Skip to content

Commit

Permalink
fix bug
Browse files Browse the repository at this point in the history
  • Loading branch information
hunterhug committed Apr 12, 2020
1 parent 72bd500 commit 58bfff2
Show file tree
Hide file tree
Showing 6 changed files with 65 additions and 27 deletions.
32 changes: 26 additions & 6 deletions algorithm/dict.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,11 +140,11 @@ func (s *Set) Add(item int) {
s.Lock()
defer s.Unlock()
s.m[item] = struct{}{} // 实际往字典添加这个键
s.len = s.len + 1 // 集合大小增加
s.len = len(s.m) // 重新计算元素数量
}
```

首先,加并发锁,实现线程安全,然后往结构体 `s *Set` 里面的内置 `map` 添加该元素:`item`,元素作为字典的键,会自动去重。同时,集合大小加1
首先,加并发锁,实现线程安全,然后往结构体 `s *Set` 里面的内置 `map` 添加该元素:`item`,元素作为字典的键,会自动去重。同时,集合大小重新生成

时间复杂度等于字典设置键值对的复杂度,哈希不冲突的时间复杂度为:`O(1)`,否则为 `O(n)`,可看哈希表实现一章。

Expand All @@ -157,11 +157,12 @@ func (s *Set) Remove(item int) {
s.Unlock()

// 集合没元素直接返回
if s.len == 0{
if s.len == 0 {
return
}

delete(s.m, item) // 实际从字典删除这个键
s.len = s.len - 1 // 集合大小减少
s.len = len(s.m) // 重新计算元素数量
}
```

Expand Down Expand Up @@ -269,15 +270,21 @@ func (s *Set) Add(item int) {
s.Lock()
defer s.Unlock()
s.m[item] = struct{}{} // 实际往字典添加这个键
s.len = s.len + 1 // 集合大小增加
s.len = len(s.m) // 重新计算元素数量
}

// 移除一个元素
func (s *Set) Remove(item int) {
s.Lock()
s.Unlock()

// 集合没元素直接返回
if s.len == 0 {
return
}

delete(s.m, item) // 实际从字典删除这个键
s.len = s.len - 1 // 集合大小减少
s.len = len(s.m) // 重新计算元素数量
}

// 查看是否存在元素
Expand Down Expand Up @@ -320,7 +327,20 @@ func (s *Set) List() []int {
return list
}

// 为什么使用空结构体
func other() {
a := struct{}{}
b := struct{}{}
if a == b {
fmt.Printf("right:%p\n", &a)
}

fmt.Println(unsafe.Sizeof(a))
}

func main() {
//other()

// 初始化一个容量为5的不可重复集合
s := NewSet(5)

Expand Down
34 changes: 23 additions & 11 deletions algorithm/search/avl_tree.md
Original file line number Diff line number Diff line change
Expand Up @@ -496,6 +496,18 @@ func (node *AVLTreeNode) MidOrder() {

后面三种情况最后都变成 `情况1`,就是将删除的节点变成叶子节点,然后可以直接删除该叶子节点,然后看其最近的父亲节点是否失衡,失衡时对树进行平衡。

举个例子,删除叶子节点,如图:

![](../../picture/avl_tree_delete1.jpg)

删除节点 `24`,导致节点 `26` 的子树不平衡了,这时需要对该子树进行旋转,旋转后如图:

![](../../picture/avl_tree_delete2.jpg)

可以发现这时树仍然不平衡,这时是节点 `22` 的子树不平衡,需要继续旋转,旋转后如图:

![](../../picture/avl_tree_delete3.jpg)

实现代码如下:

```go
Expand Down Expand Up @@ -796,19 +808,13 @@ func (node *AVLTreeNode) Delete(value int64) *AVLTreeNode {

删除操作是先找到删除的节点,然后将该节点与一个叶子节点交换,接着删除叶子节点,最后对叶子节点的父层逐层向上旋转调整。

删除操作的时间复杂度和添加操作一样。区别在于,添加操作最多旋转两次就可以达到树的平衡,而删除操作可能会旋转超过两次。如图:
删除操作的时间复杂度和添加操作一样。区别在于,添加操作最多旋转两次就可以达到树的平衡,而删除操作可能会旋转超过两次。

![](../../picture/avl_tree_delete1.jpg)
如图是一课比较糟糕的 AVL 树:

删除节点 `24`,导致节点 `26` 的子树不平衡了,这时需要对该子树进行旋转,旋转后如图:

![](../../picture/avl_tree_delete2.jpg)
![](../../picture/avl_tree_delete_worst.jpg)

可以发现这时树仍然不平衡,这时是节点 `22` 的子树不平衡,需要继续旋转,旋转后如图:

![](../../picture/avl_tree_delete3.jpg)

对于删除操作,旋转可以一直旋转到根节点,比插入旋转最多旋转两次的次数更多。
删除节点1,旋转可以一直旋转到根节点,比插入旋转最多旋转两次的次数更多。

## 六、AVL树完整代码

Expand Down Expand Up @@ -1235,4 +1241,10 @@ value: 109 tree height: 0
value: 111 tree height: -1
value: 112 tree height: -1
value: 113 tree height: 0
```
```

PS:我们的程序是递归程序,如果改写为非递归形式,效率和性能会更好,在此就不实现了,理解AVL树添加和删除的总体思路即可。

## 七、应用场景

AVL 树作为严格平衡的二叉查找树,在 `windows` 对进程地址空间的管理被使用到。
11 changes: 7 additions & 4 deletions algorithm/search/llrb_tree.md
Original file line number Diff line number Diff line change
Expand Up @@ -320,7 +320,7 @@ func (node *LLRBTNode) Add(value int64) *LLRBTNode {

`AVL` 树的最坏树高度为 `1.44log(n)`。由于左倾红黑树是近似平衡的二叉树,没有 `AVL` 树的严格平衡,树的高度会更高一点,因此查找操作效率比 `AVL` 树低,但时间复杂度只在于常数项的差别,去掉常数项,时间复杂度仍然是 `log(n)`

我们的代码实现中,左倾红黑树的插入,需要逐层判断是否需要旋转和变色,复杂度为 `log(n)`,当旋转变色后导致上层存在连续的红左链接或者红色左右链接,那么需要继续旋转和变色,可能有多次这种调整操作,如图在箭头处添加新节点,出现了右红链接,然后要一直旋转变色到根节点(穿投到根节点的情况极少发生):
我们的代码实现中,左倾红黑树的插入,需要逐层判断是否需要旋转和变色,复杂度为 `log(n)`,当旋转变色后导致上层存在连续的红左链接或者红色左右链接,那么需要继续旋转和变色,可能有多次这种调整操作,如图在箭头处添加新节点,出现了右红链接,要一直向上变色到根节点(实际上穿投到根节点的情况极少发生):

![](../../picture/llrb_tree_example.jpg)

Expand Down Expand Up @@ -621,14 +621,16 @@ func (node *LLRBTNode) FixUp() *LLRBTNode {
如果不是删除内部节点,依然是从右子树继续递归:

```go
// 删除的元素比子树根节点大,需要从右子树删除
nowNode.Right = nowNode.Right.Delete(value)
// 删除的元素比子树根节点大,需要从右子树删除
nowNode.Right = nowNode.Right.Delete(value)
```

当然,递归完成后还要进行一次 `FixUp()`
当然,递归完成后还要进行一次 `FixUp()`,恢复左倾红黑树的特征

删除操作很难理解,可以多多思考,红色左移和右移不断地递归都是为了确保删除叶子节点时,其是一个3节点。

如果不理解自顶向下的红色左移和右移递归思路,可以尝试以更根源的 `2-3树` 删除元素操作步骤来实现,这时从叶子节点开始删除,自底向上的向兄弟借值或与父亲合并,这是更容易理解的,在此我们就不实现了,可以参考普通红黑树章节的删除实现(它使用了自底向上的调整)。

完整代码见最下面。

### 2.6. 删除元素算法分析
Expand Down Expand Up @@ -1176,6 +1178,7 @@ find it 9!
not find it 9!
```

PS:我们的程序是递归程序,如果改写为非递归形式,效率和性能会更好,在此就不实现了,理解左倾红黑树添加和删除的总体思路即可。

## 三、应用场景

Expand Down
8 changes: 5 additions & 3 deletions algorithm/search/rb_tree.md
Original file line number Diff line number Diff line change
Expand Up @@ -230,7 +230,7 @@ func SetColor(node *RBTNode, color bool) {

如果是顺方向连续红链接,旋转一次即可,否则需要左右旋转或者右左旋转,旋转两次。

代码实现如下:
这次我们使用非递归的形式,效率会更高(可及时跳出循环),代码实现如下:

```go

Expand All @@ -252,7 +252,7 @@ jj

针对情况2,如果删除的叶子节点是红节点,那它对应 `2-3-4` 树的3节点或4节点,直接删除即可,删除后变为了2节点或3节点。否则,它是一个2节点,删除后破坏了平衡,要么向兄弟借值,要么和父亲的一个元素合并。

删除的叶子节点是黑色的,有以下几种情况:
删除的叶子节点是黑色的,才需要向兄弟借值,或与父亲合并,有以下几种情况:

删除的叶子节点在父亲的左边:

Expand Down Expand Up @@ -288,7 +288,9 @@ jj

类似于左边,在此不再分析。

代码实现如下:
上面的图例,我们其实可以将其画出 `2-3-4` 树,会更容易理解,在此就不画出了。

这次我们使用非递归的形式,效率会更高(可及时跳出循环),代码实现如下:

```go
jjj
Expand Down
7 changes: 4 additions & 3 deletions code/set/set.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ func (s *Set) Add(item int) {
s.Lock()
defer s.Unlock()
s.m[item] = struct{}{} // 实际往字典添加这个键
s.len = s.len + 1 // 集合大小增加
s.len = len(s.m) // 重新计算元素数量
}

// 移除一个元素
Expand All @@ -35,11 +35,12 @@ func (s *Set) Remove(item int) {
s.Unlock()

// 集合没元素直接返回
if s.len == 0{
if s.len == 0 {
return
}

delete(s.m, item) // 实际从字典删除这个键
s.len = s.len - 1 // 集合大小减少
s.len = len(s.m) // 重新计算元素数量
}

// 查看是否存在元素
Expand Down
Binary file added picture/avl_tree_delete_worst.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit 58bfff2

Please sign in to comment.