通过示例学习技术-2023-一-

通过示例学习技术 2023(一)

原文:techbyexample.com/

Jupyter notebook “Schema Not Found” 404 错误

原文:techbyexample.com/jupyter-notebook-schema-not-found/

概述

如果你是通过 homebrew 安装了 Python,那么在运行下面的 jupyter notebook 命令时,可能会遇到以下错误:

jupyter notebook

错误信息如下:

Missing or misshapen translation settings schema:
    HTTP 404: Not Found (Schema not found: /opt/homebrew/Cellar/python@3.10/3.10.12/Frameworks/Python.framework/Versions/3.10/share/jupyter/lab/schemas/@jupyterlab/translation-extension/plugin.json)

解决方案

第一种方法是导出以下内容:

export JUPYTER_PATH=/opt/homebrew/share/jupyter
export JUPYTER_CONFIG_PATH=/opt/homebrew/etc/jupyter

第二种方法是安装早期版本:

python3 -m pip install "notebook<7"

希望这能解决问题。请再次尝试运行“jupyter notebook”命令。

检查当前登录用户在 Linux 系统中所属的所有组。

原文:techbyexample.com/current-user-gropus/

groups 命令可以用来查看一个用户所属的所有组。

只需在终端中输入 groups。以下是在我的 Mac 机器上的输出。

staff everyone localaccounts _appserverusr admin

注意: 查看我们的系统设计教程系列 系统设计问题

Ruby 将字符串转换为布尔值

原文:techbyexample.com/ruby-string-bool/

概述

在 Ruby 语言中,当字符串“true”和“false”在if条件中使用时,它们会被解释为 true。见下面的示例。

if "false"
   puts "yes"
end

输出

"yes"

“false” 被评估为 true。

因此,正确处理字符串“false”或字符串“true”变得非常重要。

我们可以创建一个自定义方法,根据字符串的内容返回布尔值 true 或 false。

def true?(str)
  str.to_s.downcase == "true"
end

我们可以尝试一下上述函数。

true?("false")
 => false
true?("true")
 => true

另外,请注意,对于除“false”以外的字符串,返回的值是 true,但该函数可以轻松修改以处理这种情况。

注意: 查看我们的系统设计教程系列 System Design Questions

查看 Linux 上的当前用户

原文:techbyexample.com/current-user-linux/

概述

Linux 命令 ‘whoami’ 可以用来查看当前登录的用户。

让我们来看一下这个命令的实际操作。在终端中输入命令

whoami

它将打印当前用户。假设当前用户是john,那么运行上述命令将仅打印john

输出

whoami
john

注意: 请查看我们的系统设计教程系列 系统设计问题

如何在你的机器上找到 /bin/sh 指向的内容

原文:techbyexample.com/bin-sh-points/

概述

/bin/sh 可以是符号链接或硬链接,指向它所指向的目标。/bin/sh 可以指向

  • /bin/bash

  • /bin/dash

  • ….

如果 /bin/sh 是符号链接

如果 /bin/sh 是符号链接,我们可以使用以下命令查找它指向的目标。

file -h /bin/sh

如果 /bin/sh 是指向 bash 的符号链接,那么输出将是:

/bin/sh: symbolic link to bash

如果 /bin/sh 是硬链接

在这种情况下,我们可以使用以下命令查找 /bin/sh 指向的目标。

find -L /bin -samefile /bin/sh

如果它指向 dash,那么输出将如下:

/bin/dash
/bin/sh

例如,在 Docker 的 Ubuntu 容器中,/bin/sh 指向 /bin/dash。所以,如果你在 Docker 的 Ubuntu 容器中运行上述命令,它将输出 /bin/dash。

要在 Docker 容器中运行的命令

docker run -it ubuntu  find -L /bin -samefile /bin/sh

输出

/bin/dash
/bin/sh

注意: 请查看我们的系统设计教程系列 系统设计问题

如果任何命令失败,终止或退出一个 Shell 脚本

原文:techbyexample.com/abort-script-command-fails/

概述

如果你希望你的 Shell 脚本或 Shell 文件在任何命令返回非零值时退出,你可以在脚本开始时使用 set -e。

有时脚本中的某个命令失败,但脚本继续执行并未失败,导致出现意外的输出,因为它打破了脚本其余部分的假设,这会令人烦恼。

这是 set -e 发挥作用的地方

示例

让我们通过一个例子来理解它。我们将首先编写一个没有 set -e 的脚本。下面是一个简单的脚本。我们将其命名为 demo.sh,在这个脚本中,p 是未定义的,会导致错误。

demo.sh

p
echo "End of Script"

让我们运行这个脚本

sh demo.sh

以下是输出结果

demo.sh line 1: p: command not found
End of Script

即使脚本在第 1 行失败,echo 的最后一个命令仍然执行了。

现在让我们使用 set -e 来运行相同的脚本

demo.sh

set -e
p
echo "End of Script"

再次运行脚本。以下是输出结果

demo.sh: line 2: p: command not found

使用 set -e 时,一旦第一个命令失败,脚本会立即退出。因此,后续的 echo 命令永远不会执行

但是,在使用管道的情况下,set -e 命令不起作用。让我们看一个例子

set -e
p | echo "Hello"
echo "End of Script"

它将给出如下输出

demo.sh: line 2: p: command not found
Hello
End of Script

从上面的输出可以看到,最后一个命令确实执行了

我们必须额外使用 “-o pipefail” 标志,除了使用 “-e” 标志以外,还需要告诉 bash 即使命令在管道中失败,也退出脚本。让我们看一个例子

set -eo pipefail
p | echo "Hello"
echo "End of Script"

输出

test.sh: line 2: p: command not found
Hello

在这种情况下,echo 的最后一个命令没有执行,脚本按预期退出。

注意: 查看我们的系统设计教程系列 系统设计问题

set -e 命令的作用是什么

原文:techbyexample.com/set-e-command/

概览

如果你希望在脚本中的任何命令返回非零值时,脚本能够退出,那么可以在脚本开头使用 set -e

有时候,当脚本中的某个命令失败时,脚本依然继续执行,这会导致意外输出,因为它打破了脚本中其他部分的假设。

这时,set -e 就派上用场了

示例

让我们通过一个例子来理解它。我们将首先编写一个没有使用 set -e 的脚本。下面是一个简单的脚本。我们把它命名为 demo.sh,在这个脚本中,p 未定义,导致错误。

demo.sh

p
echo "End of Script"

让我们运行这个脚本

sh demo.sh

下面是输出结果

demo.sh line 1: p: command not found
End of Script

即使脚本在第 1 行失败,但最后的 echo 命令还是执行了。

现在我们用 set -e 运行相同的脚本

demo.sh

set -e
p
echo "End of Script"

再次运行脚本。下面是输出结果

demo.sh: line 2: p: command not found

使用 set -e 后,脚本在第一个命令失败时立即退出。因此后续的 echo 命令不会被执行

注意: 查看我们的系统设计教程系列 系统设计问题

什么是虚拟 IP

原文:techbyexample.com/virtual-ip/

目录

  • 概述

  • 虚拟 IP 如何帮助高可用性(负载均衡与故障转移)

    • 故障转移示例

    • 负载均衡示例

  • 结论

概述

在分布式系统的最终层,服务器或机器和网卡(NIC)有一个定义的物理 IP 地址。物理 IP 地址是分配给某台机器或设备的地址,无法分配给另一台物理设备。换句话说,物理 IP 地址是与特定机器绑定的。

虚拟 IP 地址不绑定到特定的机器或设备上。虚拟 IP 地址可以从一台设备移动到另一台设备,就像是一个浮动的 IP 地址。

当我们说虚拟 IP 地址属于某台服务器时,并不意味着它绑定到该服务器的网卡上。它在某一时刻分配给该服务器,而在另一个时刻同一虚拟 IP 地址可能会分配给另一台服务器。

虚拟 IP 地址的典型应用场景是当一组物理设备或服务器作为一个单独的应用或服务来工作时。

虚拟 IP 地址作为该应用程序或服务的入口点,提供负载均衡和故障转移机制,帮助实现高可用性。它如何提供这些功能,我们将在本教程中进一步探讨。

虚拟 IP 地址可以分配给 –

  • 多台服务器

  • 多个域名

  • 多个应用程序托管在单一服务器上

  • 单台服务器本身

虚拟 IP 如何帮助高可用性(负载均衡与故障转移)

我们来看两个示例

故障转移示例

虚拟 IP 地址在高可用性和故障转移中的应用举例是数据库的情况。在数据库中,通常有一个主节点和几个备份节点。主节点背后有一个特定的虚拟 IP 地址。客户端始终通过这个虚拟 IP 地址与主节点进行通信。

如果在后台,主服务器宕机,其虚拟 IP 地址将被分配给成为新主服务器的机器。这是因为虚拟 IP 地址不像物理 IP 地址那样绑定到特定的物理机器。客户端仍然与相同的虚拟 IP 地址进行通信,无法察觉主服务器已更换,且现在是另一台机器。

上述描述的是虚拟 IP 提供的故障转移机制

负载均衡示例

另一个通过提供虚拟 IP 进行负载均衡来实现高可用性的示例如下:

由于虚拟 IP 地址并不绑定到特定机器,我们可以构建一个负载均衡器,将消息发送到其后面的虚拟 IP 地址集合。负载均衡器后的每台机器都会分配一个虚拟 IP 地址。当任何机器因故障或其他原因停止工作时,它的虚拟 IP 地址可以分配给新启动的机器。

我们可以想象在负载均衡器后面有一组免费的虚拟 IP 地址。每当一个新实例启动时,它就会从这组空闲的虚拟 IP 池中获取一个虚拟 IP。

结论

这就是关于虚拟 IP 地址的全部内容。希望你喜欢这篇文章。请在评论中留下反馈。

注意: 请查看我们的系统设计教程系列 系统设计问题

点对点(P2P)与发布/订阅(Pub/Sub)在分布式消息队列中的区别

原文:techbyexample.com/p2p-pub-sub-message-queue/

概述

分布式消息队列有两种模型

  • 点对点

  • 发布/订阅

为了理解两者的区别,我们需要先理解一些术语。

  • 生产者 — 生产者是指生成需要由消费者异步消费的消息的人。

  • 消息队列 — 消息队列是生产者推送消息的地方,也是消息在被消费者读取之前保存的地方。

  • 消费者或订阅者 — 消费者是指正在消费已投递的消息的人。谈到消费者时,有一个重要的点需要注意。可能会有多个相同消费者的进程或线程在同一台或不同的机器上运行,以便并行处理消息队列中的消息。尽管“消费者”和“订阅者”可以互换使用,但在本教程中我们将使用“消费者”这一术语。

让我们来看看两者之间的区别

点对点

在点对点模型中,有一个生产者和一个消费者,且是按消息级别进行记账管理的。基本上,消息只有一个目标。例如,假设消费者有两个线程在运行。消息队列中有一条由生产者投递的消息。那么只有消费者的一个线程可以消费这条消息,一旦消费完,消息可以从队列中删除。以下是 P2P 模型的示意图

P2P

如果队列中有两条消息,那么一条消息可能会被一个线程取走,另一条消息则可能被另一个线程取走。或者可能会出现两条消息都被同一个线程取走或消费的情况。对于多于两条消息的情况也是如此。

关键点是,一条消息只能被消费者的一个线程或进程消费,消费后该消息会被删除。

也有可能是第二条消息先被处理并从队列中删除,接着第一条消息被处理并删除。因此,分布式消息队列会对已删除和未删除的消息进行无序的记账管理。

发布/订阅模型

在发布/订阅模式中,有一个生产者和多个消费者。同样,对于每个消费者,可能会有多个线程或进程在同一台或不同的机器上运行。基本上,在发布/订阅模型中,可能有多个目标接收相同的消息。下面是发布/订阅模型的示意图

Pub Sub

如果消息队列中只有一条消息,那么所有消费者都会消费这条消息。只有在所有消费者都处理完该消息后,这条消息才会从分布式消息队列中删除。

在发布-订阅模型中,一般不会按每条消息每个消费者进行账务处理。相反,分布式消息队列会维护一个偏移量,表示每个消费者已处理的消息位置。基于这个偏移量,分布式消息队列决定删除该消息。

举个例子,假设有 4 个消费者。同时假设队列中有 10 条消息。起初,所有消费者都没有处理任何消息,因此每个消费者的偏移量都是零。

消费者名称 偏移量
C1 0
C2 0
C3 0
C4 0

之后,所有消费者开始消费消息。假设 C1 能够处理所有 10 条消息,并通知消息队列它已消费并处理了这 10 条消息。同样,C2 能够处理前 5 条消息,并通知消息队列。C3 和 C4 分别能够处理前 8 条和前 4 条消息。以下是每个消费者的偏移量表:

消费者名称 偏移量
C1 10
C2 5
C3 8
C4 4

消息队列可以安全地删除前四条消息,因为它知道这四条消息已经被所有消费者消费。基本上,消息队列会查看最小的偏移量,然后根据该偏移量决定是否删除。

这里另一个需要注意的点是,消费者只能提交它们已消费的消息的偏移量。所以,如果消费者 C4 已经成功处理了消息 1、2、3、4 和 6,但未能处理消息 5,那么它只会将偏移量 4 提交给发布者,而不会提交 6。

还需要注意的是,在发布-订阅模式下,如果某个消费者的某个线程或进程正在消费某条消息,那么该消息不能被同一消费者的其他线程/进程消费。

结论

这篇文章主要讲述了分布式消息队列中 P2P 模型与发布-订阅(Pub-Sub)模型之间的主要区别。希望你喜欢这篇文章,请在评论中分享反馈。

LFU 缓存设计与实现

原文:techbyexample.com/lfu-cache-implementation-design/

概述

LFU代表最不常用的(Least Frequently Used)。在 LFU 缓存中,当缓存已满并且需要插入一个新条目时,最不常用的对象会被移除。让我们首先查看需求,然后通过示例更好地理解LFU

需求

以下是需求

  • 缓存应支持SetGet操作

  • SetGet操作的时间复杂度为 O(logn)

  • 假设缓存的最大容量是 n。一旦缓存满了并且有新的键需要插入,则必须从缓存中删除现有的一个条目

  • 删除应基于驱逐算法——LFU(最不常用)。如果两个缓存条目的使用频率或计数相同,则删除最近最少使用的条目

  • 假设缓存中的键和值都是字符串类型

LFU 示例

作为示例,假设缓存大小为 2,其中缓存条目的键和值都是字符串。假设以下是操作

  • Set (“a”, “1”) – 向缓存中添加一个元素

  • Set (“b”, “2”) – 向缓存中添加第二个元素

  • Get (“a”) – 获取键为“a”的缓存条目。键为“a”的缓存条目现在已被使用了两次,而键为“b”的缓存条目只使用了一次

  • Set (“c”, “3”) – 需要创建第三个缓存条目。缓存已经满了,因此我们需要通过 LFU 算法驱逐一个缓存条目。键为“a”的缓存条目被使用了两次,而键为“b”的缓存条目仅使用了一次。因此我们将删除键为“b”的缓存条目,因为它是最不常用的。一旦“b”被删除,我们将插入键为“c”的条目

使用的数据结构

根据需求,Set 和 Get 操作的时间复杂度应该是 O(logn)。最小堆(MinHeap)似乎是维护最不常用条目的正确数据结构。除此之外,我们还将使用映射(Map)以确保时间复杂度为 O(logn)。总体来说,将使用以下数据结构

映射(Map)

映射只能有一个键和值,其中

  • 映射的键将是缓存条目的键

  • 映射的值将是指向最小堆节点的指针。

最小堆(MinHeap)

每个最小堆节点将包含四个条目

  • 缓存条目的键(Key)

  • 缓存条目的值(Value)

  • 最小堆的索引(Index)

  • 计数(Count)表示该条目被使用了多少次

以下是这些操作的示例

  • Set (“a”, “1”)

  • Set (“b”, “2”)

  • Get (“a”)

以下将是这些数据结构中的条目

映射(Map)

{
    "a": "bff5a400", #Pointer to "a" minHeap Node
    "b": "bff5a3f6" #Pointer to "a" minHeap Node
}

最小堆(Min Heap)

[
    {
        Key: "b",    
        Value: "2",
        Index: 0,
        Count: 1    
    },
    {
        Key: "a",    
        Value: "1",
        Index: 1,
        Count: 2    
    }

]

两件事

  • “a”的计数为 2,因为它被使用了两次,即一次是通过 SET 操作,另一次是通过 GET 操作。“b”的计数为 1,因为它只在 SET 操作中使用过一次

  • “b”的索引为 0,因为它在“a”“b”中具有最低的计数

下面是我们设计中的一些高级内容。

  • 我们将有一个缓存类,作为与客户端交互的接口。

  • 缓存类将使用映射最小堆的组合来存储所有内容。使用映射和最小堆是为了确保即使在逐出操作时,GET 和 SET 操作仍然是 O(logn)的。如何实现这一点,我们将在本教程后面看到。

  • 映射将具有类型为字符串的键和指向最小堆中节点的指针类型的值,如上所述。

  • 最小堆节点将有四个条目,如上所述。

  • 将有一个逐出算法接口。将有一个LFU实现该逐出算法接口。

  • 缓存类还将嵌入一个逐出算法接口的实例。

让我们看看 Get 和 Set 如何在 O(logn)时间内工作。

Set 操作(key string,value string)

对于任何 set 操作,它首先会检查缓存中是否存在给定键的条目。可以通过查看映射来检查。如果条目存在,我们将简单地更新 Min Heap 节点中的计数和值。由于计数增加,我们将对该 MinHeap 节点进行下沉堆化(Down Heapify),因为计数已更改。

如果给定键的缓存条目不存在,它将首先创建一个具有以下详细信息的 Min Heap 节点。

  • 将是缓存条目的键。

  • 将是缓存条目的值。

  • 索引 作为(MinHeap 大小+1)。

  • 计数为 1。

一旦创建了 MinHeap 节点,就有两种情况。

  • 缓存未满 – 在这种情况下,它将把控制权交给当前的逐出算法接口。LFU 算法(逐出算法接口的实现者)将把节点插入 Min Heap 的末尾,然后调用上浮堆化(Up Heapify),使该节点达到其在 MinHeap 中的正确位置。整体操作在这里是 O(logn)。

  • 缓存已满 – 在这种情况下,它将将控制权交给 LFU 算法。它将逐出计数最少的节点。一旦该节点被逐出,它将插入新的节点。整个操作在这里是 O(logn)。

Get(key string)

对于任何 Get 操作,它将首先检查映射中是否存在给定的键。如果存在,它将获取映射中该键所指向的最小堆节点的地址。然后它将从该 MinHeap 节点中获取值。接着,它将把控制权交给当前的 LFU 算法。LFU 算法将把该 MinHeap 节点的计数增加 1,然后调用下沉堆化(Down Heapify),使该节点达到其在 MinHeap 中的正确位置。

现在让我们看一下 UML 图。

UML 图

低级设计

下面是用 GO 编程语言表达的低级设计。稍后我们将看到一个 UML 图以及一个工作示例。

缓存类

type Cache struct {
	minHeap      *minheap
	storage      map[string]*minHeapNode
	evictionAlgo evictionAlgo
	capacity     int
	maxCapacity  int
}

func initCache(evictionAlgo evictionAlgo, maxCapacity int) Cache {}

func (this *Cache) setEvictionAlgo(e evictionAlgo) {}

func (this *Cache) set(key, value string) {}

func (this *Cache) get(key string) string {}

func (this *Cache) evict() string {}

func (this *Cache) print() {}

最小堆类

type minheap struct {
	heapArray []*minHeapNode
	size      int
}

type minHeapNode struct {
	key   string
	value string
	index int
	count int
}

func newMinHeap() *minheap {}

func (this *minheap) insert(node *minHeapNode) error {}

func (this *minheap) upHeapify(index int) {}

func (this *minheap) downHeapify(current int) {}

func (this *minheap) remove() *minHeapNode {}

func (this *minheap) print() {}

逐出算法接口

type evictionAlgo interface {
	evict(c *Cache) *minHeapNode
	get(node *minHeapNode, c *Cache)
	set(node *minHeapNode, c *Cache)
	set_overwrite(node *minHeapNode, value string, c *Cache)
}

LFU 算法 – 它实现了驱逐算法接口

type lfu struct {
}

func (l *lfu) evict(c *Cache) *minHeapNode {}

func (l *lfu) get(node *minHeapNode, c *Cache) {}

func (l *lfu) set(node *minHeapNode, c *Cache) {}

func (l *lfu) set_overwrite(node *minHeapNode, value string, c *Cache) {}

完整的工作代码

如果有人对 GO 编程语言感兴趣,这里是完整的工作代码

minHeap.go

package main

import "fmt"

type minheap struct {
	heapArray []*minHeapNode
	size      int
}

type minHeapNode struct {
	key   string
	value string
	index int
	count int
}

func newMinHeap() *minheap {
	minheap := &minheap{
		heapArray: []*minHeapNode{},
		size:      0,
	}
	return minheap
}

func (this *minheap) leaf(index int) bool {
	if index >= (this.size/2) && index <= this.size {
		return true
	}
	return false
}

func (this *minheap) parent(index int) *minHeapNode {
	parentIndex := (index - 1) / 2
	return this.heapArray[parentIndex]
}

func (this *minheap) leftchild(index int) *minHeapNode {
	leftChildIndex := 2*index + 1
	if leftChildIndex > this.size-1 {
		return nil
	}
	return this.heapArray[leftChildIndex]
}

func (this *minheap) rightchild(index int) *minHeapNode {
	rightChildIndex := 2*index + 2
	if rightChildIndex > this.size-1 {
		return nil
	}
	return this.heapArray[rightChildIndex]
}

func (this *minheap) insert(node *minHeapNode) error {
	this.heapArray = append(this.heapArray, node)
	this.size++
	node.index = this.size - 1
	this.upHeapify(this.size - 1)
	return nil
}

func (this *minheap) swap(first, second *minHeapNode) {
	this.heapArray[first.index] = second
	this.heapArray[second.index] = first
	temp := first.index
	first.index = second.index
	second.index = temp
}

func (this *minheap) upHeapify(index int) {
	parentNode := this.parent(index)
	for this.heapArray[index].count < parentNode.count {
		this.swap(this.heapArray[index], this.parent(index))
	}
}

func (this *minheap) downHeapify(current int) {
	if this.leaf(current) {
		return
	}
	currNode := this.heapArray[current]
	smallest := currNode
	smallestIndex := currNode.index
	leftChildNode := this.leftchild(current)
	rightChildNode := this.rightchild(current)
	//If current is smallest then return
	if leftChildNode != nil && leftChildNode.count < smallest.count {
		smallest = leftChildNode
		smallestIndex = leftChildNode.index
	}
	if rightChildNode != nil && rightChildNode.count < smallest.count {
		smallest = rightChildNode
		smallestIndex = rightChildNode.index
	}
	if smallest != currNode {
		this.swap(currNode, smallest)
		this.downHeapify(smallestIndex)
	}
	return
}
func (this *minheap) buildMinHeap() {
	for index := ((this.size / 2) - 1); index >= 0; index-- {
		this.downHeapify(index)
	}
}

func (this *minheap) getMinimum() *minHeapNode {
	top := this.heapArray[0]
	this.heapArray[0] = this.heapArray[this.size-1]
	this.heapArray[0].index = 0
	this.heapArray = this.heapArray[:(this.size)-1]
	this.size--
	this.downHeapify(0)
	return top
}

func (this *minheap) print() {
	fmt.Println("Printing MinHeap:")
	for _, v := range this.heapArray {
		fmt.Printf("%+v\n", *v)
	}
}

evictionAlgorithm.go

package main

type evictionAlgo interface {
	evict(c *Cache) *minHeapNode
	get(node *minHeapNode, c *Cache)
	set(node *minHeapNode, c *Cache)
	set_overwrite(node *minHeapNode, value string, c *Cache)
}

func createEvictioAlgo(algoType string) evictionAlgo {
	if algoType == "lfu" {
		return &lfu{}
	}

	return nil
}

lfu.go

package main

import "fmt"

type lfu struct {
}

func (l *lfu) evict(c *Cache) *minHeapNode {
	node := c.minHeap.getMinimum()
	fmt.Printf("Evicting by lfu strtegy. Evicted Node Key: %s", node.key)
	return node
}

func (l *lfu) get(node *minHeapNode, c *Cache) {
	fmt.Printf("Shuffling node with key:%s in the minHeap due to get operation\n", node.key)
	node.count++
	c.minHeap.downHeapify(node.index)
}

func (l *lfu) set(node *minHeapNode, c *Cache) {
	fmt.Printf("Adding a new node with key:%s to minHeap due to set operation\n", node.key)
	node.count++
	c.minHeap.insert(node)
}

func (l *lfu) set_overwrite(node *minHeapNode, value string, c *Cache) {
	fmt.Printf("Shuffling node with key:%s in the minHeap due to set_overwrite operation\n", node.key)
	node.value = value
	node.count++
	c.minHeap.downHeapify(node.index)
}

cache.go

package main

import "fmt"

type Cache struct {
	minHeap      *minheap
	storage      map[string]*minHeapNode
	evictionAlgo evictionAlgo
	capacity     int
	maxCapacity  int
}

func initCache(evictionAlgo evictionAlgo, maxCapacity int) Cache {
	storage := make(map[string]*minHeapNode)
	return Cache{
		minHeap:      newMinHeap(),
		storage:      storage,
		evictionAlgo: evictionAlgo,
		capacity:     0,
		maxCapacity:  maxCapacity,
	}
}

func (this *Cache) setEvictionAlgo(e evictionAlgo) {
	this.evictionAlgo = e
}

func (this *Cache) set(key, value string) {
	node_ptr, ok := this.storage[key]
	if ok {
		this.evictionAlgo.set_overwrite(node_ptr, value, this)
		return
	}
	if this.capacity == this.maxCapacity {
		evictedKey := this.evict()
		delete(this.storage, evictedKey)
	}
	node := &minHeapNode{key: key, value: value}
	this.storage[key] = node
	this.evictionAlgo.set(node, this)
	this.capacity++
}

func (this *Cache) get(key string) string {
	node_ptr, ok := this.storage[key]
	if ok {
		this.evictionAlgo.get(node_ptr, this)
		return (*node_ptr).value
	}
	return ""
}

func (this *Cache) evict() string {
	minHeapNode := this.evictionAlgo.evict(this)
	this.capacity--
	return minHeapNode.key
}

func (this *Cache) print() {
	fmt.Println("Printing Entire Cache:")
	fmt.Println("Printing Map:")
	for k, v := range this.storage {
		fmt.Printf("key :%s value: %s\n", k, (*v).value)
	}
	this.minHeap.print()
	fmt.Println()
}

main.go

package main

import "fmt"

func main() {
	lfu := createEvictioAlgo("lfu")
	cache := initCache(lfu, 3)
	cache.set("a", "1")
	cache.print()

	cache.set("b", "2")
	cache.print()

	cache.set("c", "3")
	cache.print()

	value := cache.get("a")
	fmt.Printf("key: a, value: %s\n", value)
	cache.print()

	value = cache.get("b")
	fmt.Printf("key: a, value: %s\n", value)
	cache.print()

	value = cache.get("c")
	fmt.Printf("key: a, value: %s\n", value)
	cache.print()

	cache.set("d", "4")
	cache.print()

	cache.set("e", "5")
	cache.print()
}

输出

Adding a new node with key:a to minHeap due to set operation
Printing Entire Cache:
Printing Map:
key :a value: 1
Printing MinHeap:
{key:a value:1 index:0 count:1}

Adding a new node with key:b to minHeap due to set operation
Printing Entire Cache:
Printing Map:
key :a value: 1
key :b value: 2
Printing MinHeap:
{key:a value:1 index:0 count:1}
{key:b value:2 index:1 count:1}

Adding a new node with key:c to minHeap due to set operation
Printing Entire Cache:
Printing Map:
key :a value: 1
key :b value: 2
key :c value: 3
Printing MinHeap:
{key:a value:1 index:0 count:1}
{key:b value:2 index:1 count:1}
{key:c value:3 index:2 count:1}

Shuffling node with key:a in the minHeap due to get operation
key: a, value: 1
Printing Entire Cache:
Printing Map:
key :a value: 1
key :b value: 2
key :c value: 3
Printing MinHeap:
{key:b value:2 index:0 count:1}
{key:a value:1 index:1 count:2}
{key:c value:3 index:2 count:1}

Shuffling node with key:b in the minHeap due to get operation
key: a, value: 2
Printing Entire Cache:
Printing Map:
key :b value: 2
key :c value: 3
key :a value: 1
Printing MinHeap:
{key:c value:3 index:0 count:1}
{key:a value:1 index:1 count:2}
{key:b value:2 index:2 count:2}

Shuffling node with key:c in the minHeap due to get operation
key: a, value: 3
Printing Entire Cache:
Printing Map:
key :a value: 1
key :b value: 2
key :c value: 3
Printing MinHeap:
{key:c value:3 index:0 count:2}
{key:a value:1 index:1 count:2}
{key:b value:2 index:2 count:2}

Evicting by lfu strtegy. Evicted Node Key: cAdding a new node with key:d to minHeap due to set operation
Printing Entire Cache:
Printing Map:
key :a value: 1
key :b value: 2
key :d value: 4
Printing MinHeap:
{key:d value:4 index:0 count:1}
{key:a value:1 index:1 count:2}
{key:b value:2 index:2 count:2}

Evicting by lfu strtegy. Evicted Node Key: dAdding a new node with key:e to minHeap due to set operation
Printing Entire Cache:
Printing Map:
key :a value: 1
key :b value: 2
key :e value: 5
Printing MinHeap:
{key:e value:5 index:0 count:1}
{key:a value:1 index:1 count:2}
{key:b value:2 index:2 count:2}

完整的工作代码,放在一个文件中

这里是完整的工作代码,放在一个文件中

package main

import "fmt"

type minheap struct {
	heapArray []*minHeapNode
	size      int
}

type minHeapNode struct {
	key   string
	value string
	index int
	count int
}

func newMinHeap() *minheap {
	minheap := &minheap{
		heapArray: []*minHeapNode{},
		size:      0,
	}
	return minheap
}

func (this *minheap) leaf(index int) bool {
	if index >= (this.size/2) && index <= this.size {
		return true
	}
	return false
}

func (this *minheap) parent(index int) *minHeapNode {
	parentIndex := (index - 1) / 2
	return this.heapArray[parentIndex]
}

func (this *minheap) leftchild(index int) *minHeapNode {
	leftChildIndex := 2*index + 1
	if leftChildIndex > this.size-1 {
		return nil
	}
	return this.heapArray[leftChildIndex]
}

func (this *minheap) rightchild(index int) *minHeapNode {
	rightChildIndex := 2*index + 2
	if rightChildIndex > this.size-1 {
		return nil
	}
	return this.heapArray[rightChildIndex]
}

func (this *minheap) insert(node *minHeapNode) error {
	this.heapArray = append(this.heapArray, node)
	this.size++
	node.index = this.size - 1
	this.upHeapify(this.size - 1)
	return nil
}

func (this *minheap) swap(first, second *minHeapNode) {
	this.heapArray[first.index] = second
	this.heapArray[second.index] = first
	temp := first.index
	first.index = second.index
	second.index = temp
}

func (this *minheap) upHeapify(index int) {
	parentNode := this.parent(index)
	for this.heapArray[index].count < parentNode.count {
		this.swap(this.heapArray[index], this.parent(index))
	}
}

func (this *minheap) downHeapify(current int) {
	if this.leaf(current) {
		return
	}
	currNode := this.heapArray[current]
	smallest := currNode
	smallestIndex := currNode.index
	leftChildNode := this.leftchild(current)
	rightChildNode := this.rightchild(current)
	//If current is smallest then return
	if leftChildNode != nil && leftChildNode.count < smallest.count {
		smallest = leftChildNode
		smallestIndex = leftChildNode.index
	}
	if rightChildNode != nil && rightChildNode.count < smallest.count {
		smallest = rightChildNode
		smallestIndex = rightChildNode.index
	}
	if smallest != currNode {
		this.swap(currNode, smallest)
		this.downHeapify(smallestIndex)
	}
	return
}
func (this *minheap) buildMinHeap() {
	for index := ((this.size / 2) - 1); index >= 0; index-- {
		this.downHeapify(index)
	}
}

func (this *minheap) getMinimum() *minHeapNode {
	top := this.heapArray[0]
	this.heapArray[0] = this.heapArray[this.size-1]
	this.heapArray[0].index = 0
	this.heapArray = this.heapArray[:(this.size)-1]
	this.size--
	this.downHeapify(0)
	return top
}

func (this *minheap) print() {
	fmt.Println("Printing MinHeap:")
	for _, v := range this.heapArray {
		fmt.Printf("%+v\n", *v)
	}
}

type evictionAlgo interface {
	evict(c *Cache) *minHeapNode
	get(node *minHeapNode, c *Cache)
	set(node *minHeapNode, c *Cache)
	set_overwrite(node *minHeapNode, value string, c *Cache)
}

func createEvictioAlgo(algoType string) evictionAlgo {
	if algoType == "lfu" {
		return &lfu{}
	}

	return nil
}

type lfu struct {
}

func (l *lfu) evict(c *Cache) *minHeapNode {
	node := c.minHeap.getMinimum()
	fmt.Printf("Evicting by lfu strtegy. Evicted Node Key: %s", node.key)
	return node
}

func (l *lfu) get(node *minHeapNode, c *Cache) {
	fmt.Printf("Shuffling node with key:%s in the minHeap due to get operation\n", node.key)
	node.count++
	c.minHeap.downHeapify(node.index)
}

func (l *lfu) set(node *minHeapNode, c *Cache) {
	fmt.Printf("Adding a new node with key:%s to minHeap due to set operation\n", node.key)
	node.count++
	c.minHeap.insert(node)
}

func (l *lfu) set_overwrite(node *minHeapNode, value string, c *Cache) {
	fmt.Printf("Shuffling node with key:%s in the minHeap due to set_overwrite operation\n", node.key)
	node.value = value
	node.count++
	c.minHeap.downHeapify(node.index)
}

type Cache struct {
	minHeap      *minheap
	storage      map[string]*minHeapNode
	evictionAlgo evictionAlgo
	capacity     int
	maxCapacity  int
}

func initCache(evictionAlgo evictionAlgo, maxCapacity int) Cache {
	storage := make(map[string]*minHeapNode)
	return Cache{
		minHeap:      newMinHeap(),
		storage:      storage,
		evictionAlgo: evictionAlgo,
		capacity:     0,
		maxCapacity:  maxCapacity,
	}
}

func (this *Cache) setEvictionAlgo(e evictionAlgo) {
	this.evictionAlgo = e
}

func (this *Cache) set(key, value string) {
	node_ptr, ok := this.storage[key]
	if ok {
		this.evictionAlgo.set_overwrite(node_ptr, value, this)
		return
	}
	if this.capacity == this.maxCapacity {
		evictedKey := this.evict()
		delete(this.storage, evictedKey)
	}
	node := &minHeapNode{key: key, value: value}
	this.storage[key] = node
	this.evictionAlgo.set(node, this)
	this.capacity++
}

func (this *Cache) get(key string) string {
	node_ptr, ok := this.storage[key]
	if ok {
		this.evictionAlgo.get(node_ptr, this)
		return (*node_ptr).value
	}
	return ""
}

func (this *Cache) evict() string {
	minHeapNode := this.evictionAlgo.evict(this)
	this.capacity--
	return minHeapNode.key
}

func (this *Cache) print() {
	fmt.Println("Printing Entire Cache:")
	fmt.Println("Printing Map:")
	for k, v := range this.storage {
		fmt.Printf("key :%s value: %s\n", k, (*v).value)
	}
	this.minHeap.print()
	fmt.Println()
}

func main() {
	lfu := createEvictioAlgo("lfu")
	cache := initCache(lfu, 3)
	cache.set("a", "1")
	cache.print()

	cache.set("b", "2")
	cache.print()

	cache.set("c", "3")
	cache.print()

	value := cache.get("a")
	fmt.Printf("key: a, value: %s\n", value)
	cache.print()

	value = cache.get("b")
	fmt.Printf("key: a, value: %s\n", value)
	cache.print()

	value = cache.get("c")
	fmt.Printf("key: a, value: %s\n", value)
	cache.print()

	cache.set("d", "4")
	cache.print()

	cache.set("e", "5")
	cache.print()
}

输出

Adding a new node with key:a to minHeap due to set operation
Printing Entire Cache:
Printing Map:
key :a value: 1
Printing MinHeap:
{key:a value:1 index:0 count:1}

Adding a new node with key:b to minHeap due to set operation
Printing Entire Cache:
Printing Map:
key :b value: 2
key :a value: 1
Printing MinHeap:
{key:a value:1 index:0 count:1}
{key:b value:2 index:1 count:1}

Adding a new node with key:c to minHeap due to set operation
Printing Entire Cache:
Printing Map:
key :a value: 1
key :b value: 2
key :c value: 3
Printing MinHeap:
{key:a value:1 index:0 count:1}
{key:b value:2 index:1 count:1}
{key:c value:3 index:2 count:1}

Shuffling node with key:a in the minHeap due to get operation
key: a, value: 1
Printing Entire Cache:
Printing Map:
key :c value: 3
key :a value: 1
key :b value: 2
Printing MinHeap:
{key:b value:2 index:0 count:1}
{key:a value:1 index:1 count:2}
{key:c value:3 index:2 count:1}

Shuffling node with key:b in the minHeap due to get operation
key: a, value: 2
Printing Entire Cache:
Printing Map:
key :a value: 1
key :b value: 2
key :c value: 3
Printing MinHeap:
{key:c value:3 index:0 count:1}
{key:a value:1 index:1 count:2}
{key:b value:2 index:2 count:2}

Shuffling node with key:c in the minHeap due to get operation
key: a, value: 3
Printing Entire Cache:
Printing Map:
key :b value: 2
key :c value: 3
key :a value: 1
Printing MinHeap:
{key:c value:3 index:0 count:2}
{key:a value:1 index:1 count:2}
{key:b value:2 index:2 count:2}

Evicting by lfu strtegy. Evicted Node Key: cAdding a new node with key:d to minHeap due to set operation
Printing Entire Cache:
Printing Map:
key :d value: 4
key :a value: 1
key :b value: 2
Printing MinHeap:
{key:d value:4 index:0 count:1}
{key:a value:1 index:1 count:2}
{key:b value:2 index:2 count:2}

Evicting by lfu strtegy. Evicted Node Key: dAdding a new node with key:e to minHeap due to set operation
Printing Entire Cache:
Printing Map:
key :a value: 1
key :b value: 2
key :e value: 5
Printing MinHeap:
{key:e value:5 index:0 count:1}
{key:a value:1 index:1 count:2}
{key:b value:2 index:2 count:2}

结论

这就是关于设计基于 LFU 的内存缓存的全部内容。希望你喜欢这篇文章。请在评论中分享反馈。

注意: 请查看我们的系统设计教程系列 系统设计问题

设计一个排行榜或排行榜的低级设计

原文:techbyexample.com/leaderboard-system-design/

目录

  • 概述

  • 平衡二叉搜索树 + 哈希映射解决方案

  • 与最小堆的比较

  • 实现

    • 添加得分

    • 重置得分

    • 从排行榜中检索前 k 个和

    • 从排行榜中检索前 k 个玩家。

  • 低级设计

  • 完整工作代码

  • 结论

概述

在本教程中,我们将设计一个排行榜,并查看其完整的工作代码。以下是我们将要设计的排行榜的一些要求

  • AddScore(id, score) – 为玩家 id 为 id 的玩家添加得分。如果没有此玩家,则创建一个。

  • RestScore(id) – 重置玩家 id 为 id 的玩家得分为零

  • Top K 和 Sum – 应该能够从排行榜中检索前 k 个和。

  • Top K 玩家 – 应该能够从排行榜中检索前 k 个玩家。

看着这个问题,第一个想到的解决方案是使用最小堆 + 哈希映射。但是,如果 Top K 和 Top K 玩家调用的频率过高,最小堆就不是一个最佳解决方案,因为排行榜是用来展示领先者的。但为什么它不是一个最佳解决方案,最优的解决方案是什么呢?我们将在本教程中进行探讨。首先,让我们看看如何使用最小堆 + 哈希映射解决方案来解决这个问题

需要两个数据结构

  • 最小堆– 最小堆只会在调用 Top K 和 Sum 或 Top K 玩家方法时创建

  • 哈希映射 – 除了最小堆外,我们还将维护一个哈希映射,该映射将包含每个玩家及其得分的条目。因此,对于这个哈希映射

    • 关键将是 playerID

    • 值将是玩家的得分

让我们来看一下 Min Heap + 哈希映射解决方案中每个操作的时间复杂度

添加得分:

添加到哈希映射中。时间复杂度是 O(1)

重置得分

将哈希映射中的值设置为零。时间复杂度是 O(1)

Top K 和 Sum:

创建一个包含 k 个元素的最小堆。遍历哈希映射并将元素添加到最小堆中,如果得分大于最小堆的顶部元素。此操作的时间复杂度将是 O(Nlogk),其中 N 是玩家的数量

Top K 玩家:

与 Top K 和 Sum 相同。时间复杂度是 O(NlogK),其中 N 是玩家的数量

在这里我们可以看到,前 K 名得分和前 K 名玩家的时间复杂度是 O(Nlogk),这是相对较高的。我们可以通过使用平衡二叉搜索树 + 哈希表解决方案来优化它。对于平衡二叉搜索树 + 哈希表解决方案,下面是每个操作的时间复杂度

  • 添加得分:O(logN)

  • 重置得分:O(logN)

  • 前 K 名得分:O(K)

  • 排名前 K 的玩家:O(K)

添加得分重置得分 的时间复杂度增加了,这代价是 前 K 名得分前 K 名玩家 操作时间复杂度的降低。让我们看看如何实现它,并在教程后续部分与最小堆解决方案进行比较。

平衡二叉搜索树 + 哈希表解决方案

平衡二叉搜索树解决方案。我们将维护两种数据结构

  • 平衡二叉搜索树—我们将使用 AVL 树。在 AVL 树中的每个节点将存储以下内容

    • 得分

    • 使用该得分的玩家 ID,我们将其存储在一个映射中。为什么选择映射而不是数组?因为这样我们可以在玩家得分变化时,轻松地将玩家从节点中移除。

    • 该得分的玩家数量将等于玩家映射中条目的数量

  • 哈希表—除了平衡二叉搜索树外,我们还将维护一个哈希表,其中包含每个玩家及其得分的条目。对于这个哈希表

    • 键将是 playerID

    • 值将是玩家的得分

与最小堆的比较

以下表格比较了(平衡二叉搜索树 + 哈希表)与(最小堆 + 哈希表)的区别

假设:N 是玩家的数量

操作 平衡二叉搜索树 + 哈希表 最小堆 + 哈希表
添加得分 O(logN) O(1)
重置得分 O(logN) O(1)
前 K 名得分 O(k) O(Nlogk)
排名前 K 的玩家 O(k) O(Nlogk)

排行榜的目的是始终展示得分最高的玩家,并且每当任何玩家的得分发生变化时,榜单需要刷新。因此,我们希望进一步优化 前 K 名得分前 K 名玩家 的方法。鉴于此,平衡二叉搜索树 + 哈希表更适合设计排行榜。

实现

让我们来看一下平衡二叉搜索树(Balanced BST)+ 哈希表(Hashmap)解决方案的实现和设计。

这里是一个示例,展示如何在 AVL 树中存储数据

下面是每个操作将如何工作的示例

添加得分

对于这种情况,我们有两种情况

  • 玩家已经存在于排行榜上

对于此操作,我们将玩家同时添加到哈希表和 AVL 树中。

  • 玩家尚未出现在排行榜上

在这种情况下,意味着玩家的得分正在发生变化。所以在这种情况下,我们将做两件事。让我们通过一个示例来理解这两件事。假设玩家的原始得分是 5,现在他的得分增加了 3。那么 5 是原始得分,5+3=8 是新得分。所以我们将简单地

  • 从 AVL 树中的节点中移除该玩家,其得分为 5。

  • 将该玩家添加到 AVL 树中得分为 8 的节点。如果得分为 8 的节点不存在,则创建该节点。

AddScore 的时间复杂度为O(logN),其中 N 是玩家的数量。

重置得分

在这种情况下,我们也有两种情况

  • 玩家已在排行榜中,得分为 x

为此,我们只需从得分为 x 的 AVL 树节点中删除该玩家。检查该玩家是否存在于排行榜的映射中。如果存在,则执行上述操作。

  • 玩家尚未在排行榜中存在

在这种情况下,我们不需要做任何操作。

ResetScore 的时间复杂度为O(logN),其中 N 是玩家的数量。

从排行榜中获取前 k 名总分

为此,我们只需要从分数最高的节点开始遍历 AVL 树,然后将该分数*num_players添加到输出总和。如果该 AVL 节点的 NumPlayers 大于 k,则将分数*k添加到输出总和。继续重复此过程,依次遍历第二高分和第三高分的节点,直到所有 k 个分数都被添加。

这个操作的时间复杂度是O(K),因为它与在平衡的二叉搜索树中遍历 K 个节点是一样的。

从排行榜中获取前 k 名玩家。

为此,我们只需要从分数最高的节点开始遍历 AVL 树,然后将该分数的所有玩家添加到输出中。如果该 AVL 节点的玩家数量大于 k,则只添加 k 个玩家。

继续重复此过程,依次遍历第二高分和第三高分的节点,直到所有 k 个玩家被添加。

这个操作的时间复杂度是O(K),因为它与在平衡的二叉搜索树中遍历 K 个节点是一样的。

低级设计

在这个设计中,我们将使用两个类

  • 第一个是排行榜类。排行榜类将暴露所有方法。

    • AddScore(playerId string, scoreToAdd int)

    • Reset(playerId string)

    • TopPlayers(k int)

    • TopSum(k int)

排行榜类将内部使用 AVL 来返回所有结果。

  • 第二个是 AVL 类。这个类将包含以下两个类,并封装实现 AVL 树的所有逻辑。

    • AVL 树类

    • AVLNode 类

完整工作代码

Golang 中的 AVL 树代码灵感来自于github.com/karask/go-avltree

这里是使用 Go 编程语言实现的相应代码。

lederboard.go

package main

type leaderboard struct {
	avl       *AVLTree
	leaderMap map[string]int
}

func initLeaderboard() *leaderboard {
	return &leaderboard{
		avl:       &AVLTree{},
		leaderMap: make(map[string]int),
	}
}

func (this *leaderboard) AddScore(playerId string, scoreToAdd int) {
	oldScore, ok := this.leaderMap[playerId]
	newScore := oldScore + scoreToAdd
	if ok && oldScore != 0 {
		this.avl.Remove_Player_From_Score(oldScore, playerId)
		this.avl.Add(newScore, playerId)
	} else {
		this.avl.Add(scoreToAdd, playerId)
	}
	this.leaderMap[playerId] = newScore
}

func (this *leaderboard) Reset(playerId string) {
	oldScore, ok := this.leaderMap[playerId]
	if ok && oldScore != 0 {
		this.avl.Remove_Player_From_Score(oldScore, playerId)
	}
	this.leaderMap[playerId] = 0
}

func (this *leaderboard) TopPlayers(k int) []AVLItem {
	return this.avl.TopPlayers(k)

}

func (this *leaderboard) TopSum(k int) int {
	sum := this.avl.TopSum(k)
	return sum
}

avl.go

package main

type AVLTree struct {
	root *AVLNode
}

func (t *AVLTree) Add(key int, value string) {
	t.root = t.root.add(key, value)
}

func (t *AVLTree) Remove(key int) {
	t.root = t.root.remove(key)
}

func (t *AVLTree) Search(key int) (node *AVLNode) {
	return t.root.search(key)
}

func (t *AVLTree) TopPlayers(k int) []AVLItem {
	curr := 0
	output, _ := t.root.topPlayers(&curr, k)
	return output
}

func (t *AVLTree) TopSum(k int) int {
	curr := 0
	sum, _ := t.root.topSum(&curr, k)
	return sum
}

func (t *AVLTree) Remove_Player_From_Score(oldScore int, playerId string) {
	//Get AVL Node for old Score
	node := t.root.search(oldScore)
	if node.NumPlayers == 1 {
		t.root.remove(oldScore)
	} else {
		t.root.remove_player_from_score(oldScore, playerId)
	}
}

// AVLNode structure
type AVLNode struct {
	Score        int
	PlayerIDsMap map[string]bool
	NumPlayers   int

	// height counts nodes (not edges)
	height int
	left   *AVLNode
	right  *AVLNode
}

type AVLItem struct {
	Score     int
	PlayerIDs []string
}

func (n *AVLNode) topPlayers(curr *int, k int) ([]AVLItem, bool) {
	output := make([]AVLItem, 0)
	if n.right != nil {
		o, br := n.right.topPlayers(curr, k)
		output = append(output, o...)
		if br {
			return output, true
		}
	}

	i := 0
	playerIds := make([]string, 0)
	for playerId, _ := range n.PlayerIDsMap {
		if *curr < k && i < n.NumPlayers {
			playerIds = append(playerIds, playerId)
			*curr = *curr + 1
			i++
		} else {
			break
		}
	}

	output = append(output, AVLItem{n.Score, playerIds})

	if *curr == k {
		return output, true
	}

	if n.left != nil {
		o, br := n.left.topPlayers(curr, k)
		output = append(output, o...)
		if br {
			return output, true
		}
	}

	return output, false
}

func (n *AVLNode) topSum(curr *int, k int) (int, bool) {
	sum := 0
	if n.right != nil {
		s, br := n.right.topSum(curr, k)
		sum = sum + s
		if br {
			return sum, true
		}
	}

	less := 0

	if k-*curr < n.NumPlayers {
		less = k - *curr
	} else {
		less = n.NumPlayers
	}

	sum = sum + n.Score*less
	*curr = *curr + less

	if *curr == k {
		return sum, true
	}

	if n.left != nil {
		s, br := n.left.topSum(curr, k)
		sum = sum + s
		if br {
			return sum, true
		}
	}

	return sum, false
}

// Adds a new node
func (n *AVLNode) add(score int, playerID string) *AVLNode {
	if n == nil {
		m := make(map[string]bool)
		m[playerID] = true
		return &AVLNode{score, m, 1, 1, nil, nil}
	}

	if score < n.Score {
		n.left = n.left.add(score, playerID)
	} else if score > n.Score {
		n.right = n.right.add(score, playerID)
	} else {
		// if same key exists update value
		n.NumPlayers = n.NumPlayers + 1
		n.PlayerIDsMap[playerID] = true
	}
	return n.rebalanceTree()
}

// Removes a node
func (n *AVLNode) remove(score int) *AVLNode {
	if n == nil {
		return nil
	}
	if score < n.Score {
		n.left = n.left.remove(score)
	} else if score > n.Score {
		n.right = n.right.remove(score)
	} else {
		if n.left != nil && n.right != nil {
			// node to delete found with both children;
			// replace values with smallest node of the right sub-tree
			rightMinNode := n.right.findSmallest()
			n.Score = rightMinNode.Score
			n.PlayerIDsMap = rightMinNode.PlayerIDsMap
			n.NumPlayers = rightMinNode.NumPlayers
			// delete smallest node that we replaced
			n.right = n.right.remove(rightMinNode.Score)
		} else if n.left != nil {
			// node only has left child
			n = n.left
		} else if n.right != nil {
			// node only has right child
			n = n.right
		} else {
			// node has no children
			n = nil
			return n
		}

	}
	return n.rebalanceTree()
}

// Remove player from score
func (n *AVLNode) remove_player_from_score(score int, playerId string) {
	if n == nil {
		return
	}
	if score < n.Score {
		n.left.remove_player_from_score(score, playerId)
	} else if score > n.Score {
		n.right.remove_player_from_score(score, playerId)
	} else {
		n.NumPlayers = n.NumPlayers - 1
		delete(n.PlayerIDsMap, playerId)
	}
	return
}

// Searches for a node
func (n *AVLNode) search(score int) *AVLNode {
	if n == nil {
		return nil
	}
	if score < n.Score {
		return n.left.search(score)
	} else if score > n.Score {
		return n.right.search(score)
	} else {
		return n
	}
}

func (n *AVLNode) getHeight() int {
	if n == nil {
		return 0
	}
	return n.height
}

func (n *AVLNode) recalculateHeight() {
	n.height = 1 + max(n.left.getHeight(), n.right.getHeight())
}

// Checks if node is balanced and rebalance
func (n *AVLNode) rebalanceTree() *AVLNode {
	if n == nil {
		return n
	}
	n.recalculateHeight()

	// check balance factor and rotateLeft if right-heavy and rotateRight if left-heavy
	balanceFactor := n.left.getHeight() - n.right.getHeight()
	if balanceFactor == -2 {
		// check if child is left-heavy and rotateRight first
		if n.right.left.getHeight() > n.right.right.getHeight() {
			n.right = n.right.rotateRight()
		}
		return n.rotateLeft()
	} else if balanceFactor == 2 {
		// check if child is right-heavy and rotateLeft first
		if n.left.right.getHeight() > n.left.left.getHeight() {
			n.left = n.left.rotateLeft()
		}
		return n.rotateRight()
	}
	return n
}

// Rotate nodes left to balance node
func (n *AVLNode) rotateLeft() *AVLNode {
	newRoot := n.right
	n.right = newRoot.left
	newRoot.left = n

	n.recalculateHeight()
	newRoot.recalculateHeight()
	return newRoot
}

// Rotate nodes right to balance node
func (n *AVLNode) rotateRight() *AVLNode {
	newRoot := n.left
	n.left = newRoot.right
	newRoot.right = n

	n.recalculateHeight()
	newRoot.recalculateHeight()
	return newRoot
}

// Finds the smallest child (based on the key) for the current node
func (n *AVLNode) findSmallest() *AVLNode {
	if n.left != nil {
		return n.left.findSmallest()
	} else {
		return n
	}
}

// Returns max number - TODO: std lib seemed to only have a method for floats!
func max(a int, b int) int {
	if a > b {
		return a
	}
	return b
}

main.go

package main

import "fmt"

func main() {
	leaderboard := initLeaderboard()

	leaderboard.AddScore("a", 1)
	leaderboard.AddScore("b", 2)
	leaderboard.AddScore("c", 3)
	leaderboard.AddScore("d", 4)
	leaderboard.AddScore("e", 4)

	leaderboard.AddScore("f", 10)

	k := 4
	output := leaderboard.TopPlayers(k)
	for i := 0; i < len(output); i++ {
		fmt.Printf("PlayerIDs: %v, Score: %d\n", output[i].PlayerIDs, output[i].Score)
	}
	topSum := leaderboard.TopSum(k)
	fmt.Printf("Sum: %d", topSum)

	leaderboard.AddScore("f", 15)

	fmt.Println("\n")
	k = 7
	output = leaderboard.TopPlayers(k)
	for i := 0; i < len(output); i++ {
		fmt.Printf("PlayerIDs: %v, Score: %d\n", output[i].PlayerIDs, output[i].Score)
	}
	topSum = leaderboard.TopSum(k)
	fmt.Printf("Sum: %d", topSum)

}

输出

PlayerIDs: [f], Score: 10
PlayerIDs: [d e], Score: 4
PlayerIDs: [c], Score: 3
Sum: 21

PlayerIDs: [f], Score: 25
PlayerIDs: [e d], Score: 4
PlayerIDs: [c], Score: 3
PlayerIDs: [b], Score: 2
PlayerIDs: [a], Score: 1
Sum: 39

结论

这篇文章主要讲解了如何设计和实现一个排行榜。希望你喜欢这篇文章,请在评论中分享反馈。

注意: 请查看我们的系统设计教程系列系统设计问题

蛇与梯子游戏的系统设计

原文:techbyexample.com/snake-ladder-game-oops-design/

目录

  • 概览

  • 蛇与梯子游戏中的所有角色

  • 需求分析

  • UML 图

  • 低级设计

  • 完整工作代码

  • 单文件中的完整工作代码

  • 结论

概览

设计蛇与梯子游戏是一个低级设计问题,应该以这种方式解决。在这个问题中,面试官希望测试你的面向对象技能。

你可以在这里了解更多关于蛇与梯子游戏的信息 – en.wikipedia.org/wiki/Snakes_and_ladders

在本教程中,我们将讨论蛇与梯子游戏的低级设计。同时,我们还将查看蛇与梯子游戏的完整工作代码。以下是本教程的目录

  • 蛇与梯子游戏中的所有角色

  • 需求分析

  • UML 图

  • 用 Go 编程语言表示的低级设计

  • Go 语言中的完整工作代码

  • 单文件中的完整工作代码

蛇与梯子游戏中的所有角色

让我们确定设计中存在的所有主要角色

  • 棋盘 – 棋盘将从 1 到 100 编号,简单地表示游戏中的棋盘。

  • 游戏 – 它是控制游戏整体流程的主要类。

  • 玩家 – 它是一个表示玩家的接口。将有两个具体类实现这个接口。

    • 人类玩家

    • 计算机玩家

  • – 它表示一条蛇

  • 梯子 – 它表示一把梯子

需求分析

除了这里提到的蛇与梯子游戏规则 en.wikipedia.org/wiki/Snakes_and_ladders,我们还将以可扩展的方式进行设计,其中 –

  • 游戏可以由任意数量的玩家进行

  • 系统应支持任意数量的梯子和蛇。

  • 游戏应该支持任何数量的骰子掷投

UML 图

以下是蛇与梯子游戏的 UML 图。

蛇与梯子游戏的 UML 图

蛇与梯子游戏的 UML 图

让我们理解这个 UML 图

上述 UML 图中最简单的组件是 iPlayer 接口。它定义了以下功能

  • 掷骰子 – 玩家将掷骰子并返回骰子点数。它将骰子的数量作为参数

  • 设置位置 – 通过这种方法设置玩家的位置。玩家的位置将从 1 到 n*n,其中 n 是棋盘的维度

  • 获取位置 – 一个用于获取位置的工具方法

  • 获取 ID – 一个简单的函数,用于获取玩家的 ID

该接口由两个类实现

  • 人类玩家

  • 计算机玩家

接下来是梯子类。这些类仅用于创建和验证蛇和梯子。通过验证,我们的意思是,对于蛇,我们将验证蛇的起始点是否大于蛇的终点。对于梯子,我们将验证梯子的起始点是否小于梯子的终点。

创建的蛇和梯子将依次传递到棋盘的创建中,棋盘内部实现了蛇和梯子作为一个映射。

接下来是棋盘类本身。它将包含以下字段

  • 一个 n*n 的方格,其中每个方格都是符号类类型。符号类只包含玩家 ID 映射。例如,假设有两个玩家在玩游戏。在游戏过程中,两个玩家可能都在方格 5 中。玩家在位置 5 的位置由符号类中的玩家 ID 映射表示。

  • 维度 – 棋盘的维度

  • 蛇图 – 表示棋盘上的蛇。地图中的是蛇的起始点,是蛇的终点

  • 梯子图 – 表示棋盘上的梯子。地图中的是梯子的起始点,是梯子的终点

有一个游戏状态枚举类,定义了游戏的不同状态。游戏可以处于进行中游戏结束状态。

接下来是游戏类,它是驱动类。它包含以下组件

  • 棋盘 – 棋盘本身

  • 玩家 – 玩游戏的玩家列表

  • 已移动次数 – 到目前为止已经发生的移动次数

  • 游戏状态 – 此时的游戏状态是什么

  • 获胜玩家 – 最终获胜的玩家是谁

  • 骰子数量 – 游戏使用的骰子数量

现在让我们看看低级设计,以下是用 Go 编程语言表示的

低级设计

以下是用 Go 编程语言表示的低级设计

iPlayer 接口

type iPlayer interface {
	rollDice(numDice int) (diceValue int)
	getID() (playerId string)
	setPosition(newPosition int)
	getPosition() (currPosition int)
}

人类玩家类

type humanPlayer struct {
	position int
	id       string
}

func (h *humanPlayer) rollDice(numDice int) int {}

func (h *humanPlayer) getID() string {}

func (h *humanPlayer) setPosition(newPosition int) {}

func (h *humanPlayer) getPosition() int {}

计算机玩家类

type computerPlayer struct {
	position int
	id       string
}

func (h *computerPlayer) rollDice(numDice int) int {}

func (h *computerPlayer) getID() string {}

func (h *computerPlayer) setPosition(newPosition int) {}

func (h *computerPlayer) getPosition() int {}

游戏状态枚举

type GameStatus uint8

const (
	GameInProgress GameStatus = iota
	GameFinished
)

蛇类(仅用于创建和验证蛇)

type snake struct {
	start int
	end   int
}

func createSnake(start, end int) (snake, error) {}

梯子类(仅用于创建和验证梯子)

type ladder struct {
	start int
	end   int
}

func createLadder(start, end int) (ladder, error) {}

符号类

type Symbol struct {
	playersIdMap map[string]bool
}

func (s Symbol) getRepresentation(x, y, dimension int) string {}

棋盘类

type board struct {
	square    [][]Symbol
	dimension int
	snakeMap  map[int]int
	ladderMap map[int]int
}

func (b *board) makeMove(player iPlayer, diceNum int) (playerWon bool) {}

func (b *board) removePlayerFromPosition(player iPlayer, position int) {}

func (b *board) addPlayerToPosition(player iPlayer, position int) {}

func (b *board) printBoard() {
	line := "-------------------------------------------------------------"
	fmt.Println(line)
	for i := b.dimension - 1; i >= 0; i-- {
		fmt.Print("|")
		for j := b.dimension - 1; j >= 0; j-- {
			fmt.Print(b.square[i][j].getRepresentation(i, j, b.dimension))
		}
		fmt.Println()
	}
	fmt.Println(line)
	fmt.Println()
}

游戏类

type game struct {
	board         *board
	players       []iPlayer
	numMoves      int
	gameStatus    GameStatus
	winningPlayer iPlayer
	numDice       int
}

func initGame(b *board, players []iPlayer, numDice int) *game {
	game := &game{
		board:      b,
		players:    players,
		gameStatus: GameInProgress,
		numDice:    numDice,
		numMoves:   0,
	}
	return game
}

func (g *game) play() error {}

func (g *game) printResult() {}

完整工作代码

这里是完整的工作代码

iPlayer.go

package main

type iPlayer interface {
	rollDice(numDice int) (diceValue int)
	getID() (playerId string)
	setPosition(newPosition int)
	getPosition() (currPosition int)
}

humanPlayer.go

package main

import (
	"math/rand"
	"time"
)

type humanPlayer struct {
	position int
	id       string
}

func (h *humanPlayer) rollDice(numDice int) int {
	diceValue := 0

	for i := 0; i < numDice; i++ {
		rand.Seed(time.Now().UnixNano())
		diceValue += rand.Intn(6) + 1
	}
	return diceValue
}

func (h *humanPlayer) getID() string {
	return h.id
}

func (h *humanPlayer) setPosition(newPosition int) {
	h.position = newPosition
}

func (h *humanPlayer) getPosition() int {
	return h.position
}

computerPlayer.go

package main

import (
	"math/rand"
	"time"
)

type computerPlayer struct {
	position int
	id       string
}

func (h *computerPlayer) rollDice(numDice int) int {
	diceValue := 0

	for i := 0; i < numDice; i++ {
		rand.Seed(time.Now().UnixNano())
		diceValue += rand.Intn(6) + 1
	}
	return diceValue
}

func (h *computerPlayer) getID() string {
	return h.id
}

func (h *computerPlayer) setPosition(newPosition int) {
	h.position = newPosition
}

func (h *computerPlayer) getPosition() int {
	return h.position
}

gameStatus.go

package main

type GameStatus uint8

const (
	GameInProgress GameStatus = iota
	GameFinished
)

symbol.go

package main

import (
	"strconv"
	"strings"
)

type Symbol struct {
	playersIdMap map[string]bool
}

func (s Symbol) getRepresentation(x, y, dimension int) string {
	val := ""
	playerIds := make([]string, 0)
	for k, v := range s.playersIdMap {
		if v {
			playerIds = append(playerIds, k)
		}
	}
	if len(playerIds) > 0 {
		val = strings.Join(playerIds, ",")
	} else {
		val = "."
	}
	num := x*dimension + y + 1
	strr := strconv.Itoa(num)
	rem := 3 - len(strr)
	for i := 0; i < rem; i++ {
		strr = strr + " "
	}
	strr = strr + " " + val + "|"
	return strr
}

snake.go

package main

import "fmt"

type snake struct {
	start int
	end   int
}

func createSnake(start, end int) (snake, error) {
	if end >= start {
		return snake{}, fmt.Errorf("End cannot be greater than Start for a snake")
	}

	snake := snake{
		start: start,
		end:   end,
	}

	return snake, nil
}

ladder.go

package main

import "fmt"

type ladder struct {
	start int
	end   int
}

func createLadder(start, end int) (ladder, error) {
	if start >= end {
		return ladder{}, fmt.Errorf("Start cannot be greater than End for a ladder")
	}

	ladder := ladder{
		start: start,
		end:   end,
	}

	return ladder, nil
}

board.go

package main

import (
	"fmt"
	"strconv"
	"strings"
)

type board struct {
	square    [][]Symbol
	dimension int
	snakeMap  map[int]int
	ladderMap map[int]int
}

func createBoard(snakes []snake, ladders []ladder, dimension int, players []iPlayer) *board {
	snakeMap := make(map[int]int)
	ladderMap := make(map[int]int)

	for i := 0; i < len(snakes); i++ {
		snakeMap[snakes[i].start] = snakes[i].end
	}

	for i := 0; i < len(ladders); i++ {
		ladderMap[ladders[i].start] = ladders[i].end
	}

	square := make([][]Symbol, dimension)
	for i := 0; i < dimension; i++ {
		square[i] = make([]Symbol, dimension)
	}

	for i := 0; i < dimension; i++ {
		for j := 0; j < dimension; j++ {
			square[i][j] = Symbol{
				playersIdMap: make(map[string]bool),
			}
		}
	}

	playersIdMap := make(map[string]bool)
	for i := 0; i < len(players); i++ {
		player := players[i]
		playersIdMap[player.getID()] = true
	}
	square[0][0] = Symbol{
		playersIdMap: playersIdMap,
	}

	return &board{
		square:    square,
		dimension: dimension,
		snakeMap:  snakeMap,
		ladderMap: ladderMap,
	}
}

func (b *board) markPosition(player iPlayer, currPosition, newPosition int) {
	player.setPosition(newPosition)
	b.removePlayerFromPosition(player, currPosition)
	b.addPlayerToPosition(player, newPosition)
}

func (b *board) removePlayerFromPosition(player iPlayer, position int) {
	x := (position - 1) / b.dimension
	y := (position - 1) % b.dimension
	currSymbol := b.square[x][y]
	delete(currSymbol.playersIdMap, player.getID())
	b.square[x][y] = currSymbol
}

func (b *board) addPlayerToPosition(player iPlayer, position int) {
	x := (position - 1) / b.dimension
	y := (position - 1) % b.dimension
	currSymbol := b.square[x][y]
	currSymbol.playersIdMap[player.getID()] = true
	b.square[x][y] = currSymbol
}

func (b *board) makeMove(player iPlayer, diceNum int) (playerWon bool) {
	currPosition := player.getPosition()
	newPosition := currPosition + diceNum

	if newPosition == b.dimension*b.dimension {
		b.printMoveNormal(player, currPosition, newPosition, diceNum)
		b.markPosition(player, currPosition, newPosition)
		playerWon = true
		return playerWon
	}

	if newPosition > b.dimension*b.dimension {
		newPosition = currPosition
		b.printMoveNormal(player, currPosition, newPosition, diceNum)
		b.markPosition(player, currPosition, newPosition)
		return false
	}

	end, ok := b.snakeMap[newPosition]

	if ok {
		snakeBitPosition := newPosition
		newPosition = end
		b.markPosition(player, currPosition, newPosition)
		b.printMoveSnake(player, currPosition, snakeBitPosition, diceNum, snakeBitPosition, b.snakeMap[snakeBitPosition])
		return false
	}

	end, ok = b.ladderMap[newPosition]

	if ok {
		ladderClimbPosition := newPosition
		newPosition = end
		b.markPosition(player, currPosition, newPosition)
		b.printMoveLadder(player, currPosition, ladderClimbPosition, diceNum, ladderClimbPosition, b.ladderMap[ladderClimbPosition])
		return false
	}

	b.printMoveNormal(player, currPosition, newPosition, diceNum)
	b.markPosition(player, currPosition, newPosition)
	player.setPosition(newPosition)
	return false
}

func (g *board) printMoveNormal(player iPlayer, currPosition, newPosition, diceNum int) {
	fmt.Printf("Player with Id:%s moved from position:%d to position:%d after rolling dice with num:%d\n", player.getID(), currPosition, newPosition, diceNum)
}

func (g *board) printMoveSnake(player iPlayer, currPosition, snakeBitPosition, diceNum, snakeStart, snakeEnd int) {
	fmt.Printf("Player with Id:%s moved from position:%d to position:%d after rolling dice with num:%d and then to position:%d. Bit by snake, snakeStart:%d snakeEnd:%d\n", player.getID(), currPosition, snakeBitPosition, diceNum, snakeEnd, snakeStart, snakeEnd)
}

func (g *board) printMoveLadder(player iPlayer, currPosition, ladderClimbPosition, diceNum, ladderStart, ladderEnd int) {
	fmt.Printf("Player with Id:%s moved from position:%d to position:%d after rolling dice with num:%d and then to position:%d. Climbed by ladder, ladderStart:%d ladderEnd:%d\n", player.getID(), currPosition, ladderClimbPosition, diceNum, ladderEnd, ladderStart, ladderEnd)
}

func (b *board) printBoard() {
	line := "-------------------------------------------------------------"
	fmt.Println(line)
	for i := b.dimension - 1; i >= 0; i-- {
		fmt.Print("|")
		for j := b.dimension - 1; j >= 0; j-- {
			fmt.Print(b.square[i][j].getRepresentation(i, j, b.dimension))
		}
		fmt.Println()
	}
	fmt.Println(line)
	fmt.Println()
}

func IntArrayToString(a []int) string {
	b := make([]string, len(a))
	for i, v := range a {
		b[i] = strconv.Itoa(v)
	}

	return strings.Join(b, ",")
}

game.go

package main

import "fmt"

type game struct {
	board         *board
	players       []iPlayer
	numMoves      int
	gameStatus    GameStatus
	winningPlayer iPlayer
	numDice       int
}

func initGame(b *board, players []iPlayer, numDice int) *game {
	game := &game{
		board:      b,
		players:    players,
		gameStatus: GameInProgress,
		numDice:    numDice,
		numMoves:   0,
	}
	return game
}

func (g *game) play() error {
	fmt.Println("Initial Board")
	g.board.printBoard()
	for {
		for i := 0; i < len(g.players); i++ {
			currPlayer := g.players[i]
			diceNum := currPlayer.rollDice(g.numDice)
			playerWon := g.board.makeMove(currPlayer, diceNum)
			if playerWon {
				g.winningPlayer = currPlayer
				g.gameStatus = GameFinished
				break
			}
			g.board.printBoard()
		}

		g.numMoves = g.numMoves + 1
		if g.gameStatus != GameInProgress {
			break
		}
	}
	return nil
}

func (g *game) printResult() {
	switch g.gameStatus {
	case GameInProgress:
		fmt.Println("Game in Progress")
	case GameFinished:
		fmt.Println("Game Finished")
		fmt.Printf("Players with ID:%s won\n", g.winningPlayer.getID())
	default:
		fmt.Println("Invalid Game Status")
	}
	g.board.printBoard()
}

main.go

package main

import "fmt"

func main() {

	//Create 4 ladder

	snakes, err := createSnakes()
	if err != nil {
		fmt.Println(err)
	}

	ladders, err := createLadders()
	if err != nil {
		fmt.Println(err)
	}

	player1 := &humanPlayer{
		position: 1,
		id:       "a",
	}

	player2 := &computerPlayer{
		position: 1,
		id:       "b",
	}

	board := createBoard(snakes, ladders, 10, []iPlayer{player1, player2})

	game := initGame(board, []iPlayer{player1, player2}, 1)

	game.play()
	game.printResult()

}

func createSnakes() ([]snake, error) {
	// Create 4 snakes
	s1, err := createSnake(35, 25)
	if err != nil {
		fmt.Println(err)
		return nil, err
	}
	s2, err := createSnake(56, 36)
	if err != nil {
		fmt.Println(err)
		return nil, err
	}
	s3, err := createSnake(77, 57)
	if err != nil {
		fmt.Println(err)
		return nil, err
	}
	s4, err := createSnake(98, 78)
	if err != nil {
		fmt.Println(err)
		return nil, err
	}
	snakes := []snake{s1, s2, s3, s4}
	return snakes, nil
}

func createLadders() ([]ladder, error) {
	// Create 4 snakes
	s1, err := createLadder(26, 36)
	if err != nil {
		fmt.Println(err)
		return nil, err
	}
	s2, err := createLadder(37, 57)
	if err != nil {
		fmt.Println(err)
		return nil, err
	}
	s3, err := createLadder(58, 78)
	if err != nil {
		fmt.Println(err)
		return nil, err
	}
	s4, err := createLadder(74, 94)
	if err != nil {
		fmt.Println(err)
		return nil, err
	}
	ladders := []ladder{s1, s2, s3, s4}
	return ladders, nil
}

让我们运行这个程序

请注意,每次运行此程序时,输出都会有所不同

输出

Initial Board
-------------------------------------------------------------
|100 .|99  .|98  .|97  .|96  .|95  .|94  .|93  .|92  .|91  .|
|90  .|89  .|88  .|87  .|86  .|85  .|84  .|83  .|82  .|81  .|
|80  .|79  .|78  .|77  .|76  .|75  .|74  .|73  .|72  .|71  .|
|70  .|69  .|68  .|67  .|66  .|65  .|64  .|63  .|62  .|61  .|
|60  .|59  .|58  .|57  .|56  .|55  .|54  .|53  .|52  .|51  .|
|50  .|49  .|48  .|47  .|46  .|45  .|44  .|43  .|42  .|41  .|
|40  .|39  .|38  .|37  .|36  .|35  .|34  .|33  .|32  .|31  .|
|30  .|29  .|28  .|27  .|26  .|25  .|24  .|23  .|22  .|21  .|
|20  .|19  .|18  .|17  .|16  .|15  .|14  .|13  .|12  .|11  .|
|10  .|9   .|8   .|7   .|6   .|5   .|4   .|3   .|2   .|1   a,b|
-------------------------------------------------------------

Player with Id:a moved from position:1 to position:6 after rolling dice with num:5
-------------------------------------------------------------
|100 .|99  .|98  .|97  .|96  .|95  .|94  .|93  .|92  .|91  .|
|90  .|89  .|88  .|87  .|86  .|85  .|84  .|83  .|82  .|81  .|
|80  .|79  .|78  .|77  .|76  .|75  .|74  .|73  .|72  .|71  .|
|70  .|69  .|68  .|67  .|66  .|65  .|64  .|63  .|62  .|61  .|
|60  .|59  .|58  .|57  .|56  .|55  .|54  .|53  .|52  .|51  .|
|50  .|49  .|48  .|47  .|46  .|45  .|44  .|43  .|42  .|41  .|
|40  .|39  .|38  .|37  .|36  .|35  .|34  .|33  .|32  .|31  .|
|30  .|29  .|28  .|27  .|26  .|25  .|24  .|23  .|22  .|21  .|
|20  .|19  .|18  .|17  .|16  .|15  .|14  .|13  .|12  .|11  .|
|10  .|9   .|8   .|7   .|6   a|5   .|4   .|3   .|2   .|1   b|
-------------------------------------------------------------

Player with Id:b moved from position:1 to position:3 after rolling dice with num:2
-------------------------------------------------------------
|100 .|99  .|98  .|97  .|96  .|95  .|94  .|93  .|92  .|91  .|
|90  .|89  .|88  .|87  .|86  .|85  .|84  .|83  .|82  .|81  .|
|80  .|79  .|78  .|77  .|76  .|75  .|74  .|73  .|72  .|71  .|
|70  .|69  .|68  .|67  .|66  .|65  .|64  .|63  .|62  .|61  .|
|60  .|59  .|58  .|57  .|56  .|55  .|54  .|53  .|52  .|51  .|
|50  .|49  .|48  .|47  .|46  .|45  .|44  .|43  .|42  .|41  .|
|40  .|39  .|38  .|37  .|36  .|35  .|34  .|33  .|32  .|31  .|
|30  .|29  .|28  .|27  .|26  .|25  .|24  .|23  .|22  .|21  .|
|20  .|19  .|18  .|17  .|16  .|15  .|14  .|13  .|12  .|11  .|
|10  .|9   .|8   .|7   .|6   a|5   .|4   .|3   b|2   .|1   .|
-------------------------------------------------------------

Player with Id:a moved from position:6 to position:8 after rolling dice with num:2
-------------------------------------------------------------
|100 .|99  .|98  .|97  .|96  .|95  .|94  .|93  .|92  .|91  .|
|90  .|89  .|88  .|87  .|86  .|85  .|84  .|83  .|82  .|81  .|
|80  .|79  .|78  .|77  .|76  .|75  .|74  .|73  .|72  .|71  .|
|70  .|69  .|68  .|67  .|66  .|65  .|64  .|63  .|62  .|61  .|
|60  .|59  .|58  .|57  .|56  .|55  .|54  .|53  .|52  .|51  .|
|50  .|49  .|48  .|47  .|46  .|45  .|44  .|43  .|42  .|41  .|
|40  .|39  .|38  .|37  .|36  .|35  .|34  .|33  .|32  .|31  .|
|30  .|29  .|28  .|27  .|26  .|25  .|24  .|23  .|22  .|21  .|
|20  .|19  .|18  .|17  .|16  .|15  .|14  .|13  .|12  .|11  .|
|10  .|9   .|8   a|7   .|6   .|5   .|4   .|3   b|2   .|1   .|
-------------------------------------------------------------

Player with Id:b moved from position:3 to position:5 after rolling dice with num:2
-------------------------------------------------------------
|100 .|99  .|98  .|97  .|96  .|95  .|94  .|93  .|92  .|91  .|
|90  .|89  .|88  .|87  .|86  .|85  .|84  .|83  .|82  .|81  .|
|80  .|79  .|78  .|77  .|76  .|75  .|74  .|73  .|72  .|71  .|
|70  .|69  .|68  .|67  .|66  .|65  .|64  .|63  .|62  .|61  .|
|60  .|59  .|58  .|57  .|56  .|55  .|54  .|53  .|52  .|51  .|
|50  .|49  .|48  .|47  .|46  .|45  .|44  .|43  .|42  .|41  .|
|40  .|39  .|38  .|37  .|36  .|35  .|34  .|33  .|32  .|31  .|
|30  .|29  .|28  .|27  .|26  .|25  .|24  .|23  .|22  .|21  .|
|20  .|19  .|18  .|17  .|16  .|15  .|14  .|13  .|12  .|11  .|
|10  .|9   .|8   a|7   .|6   .|5   b|4   .|3   .|2   .|1   .|
-------------------------------------------------------------

Player with Id:a moved from position:8 to position:10 after rolling dice with num:2
-------------------------------------------------------------
|100 .|99  .|98  .|97  .|96  .|95  .|94  .|93  .|92  .|91  .|
|90  .|89  .|88  .|87  .|86  .|85  .|84  .|83  .|82  .|81  .|
|80  .|79  .|78  .|77  .|76  .|75  .|74  .|73  .|72  .|71  .|
|70  .|69  .|68  .|67  .|66  .|65  .|64  .|63  .|62  .|61  .|
|60  .|59  .|58  .|57  .|56  .|55  .|54  .|53  .|52  .|51  .|
|50  .|49  .|48  .|47  .|46  .|45  .|44  .|43  .|42  .|41  .|
|40  .|39  .|38  .|37  .|36  .|35  .|34  .|33  .|32  .|31  .|
|30  .|29  .|28  .|27  .|26  .|25  .|24  .|23  .|22  .|21  .|
|20  .|19  .|18  .|17  .|16  .|15  .|14  .|13  .|12  .|11  .|
|10  a|9   .|8   .|7   .|6   .|5   b|4   .|3   .|2   .|1   .|
-------------------------------------------------------------

Player with Id:b moved from position:5 to position:10 after rolling dice with num:5
-------------------------------------------------------------
|100 .|99  .|98  .|97  .|96  .|95  .|94  .|93  .|92  .|91  .|
|90  .|89  .|88  .|87  .|86  .|85  .|84  .|83  .|82  .|81  .|
|80  .|79  .|78  .|77  .|76  .|75  .|74  .|73  .|72  .|71  .|
|70  .|69  .|68  .|67  .|66  .|65  .|64  .|63  .|62  .|61  .|
|60  .|59  .|58  .|57  .|56  .|55  .|54  .|53  .|52  .|51  .|
|50  .|49  .|48  .|47  .|46  .|45  .|44  .|43  .|42  .|41  .|
|40  .|39  .|38  .|37  .|36  .|35  .|34  .|33  .|32  .|31  .|
|30  .|29  .|28  .|27  .|26  .|25  .|24  .|23  .|22  .|21  .|
|20  .|19  .|18  .|17  .|16  .|15  .|14  .|13  .|12  .|11  .|
|10  a,b|9   .|8   .|7   .|6   .|5   .|4   .|3   .|2   .|1   .|
-------------------------------------------------------------

Player with Id:a moved from position:10 to position:13 after rolling dice with num:3
-------------------------------------------------------------
|100 .|99  .|98  .|97  .|96  .|95  .|94  .|93  .|92  .|91  .|
|90  .|89  .|88  .|87  .|86  .|85  .|84  .|83  .|82  .|81  .|
|80  .|79  .|78  .|77  .|76  .|75  .|74  .|73  .|72  .|71  .|
|70  .|69  .|68  .|67  .|66  .|65  .|64  .|63  .|62  .|61  .|
|60  .|59  .|58  .|57  .|56  .|55  .|54  .|53  .|52  .|51  .|
|50  .|49  .|48  .|47  .|46  .|45  .|44  .|43  .|42  .|41  .|
|40  .|39  .|38  .|37  .|36  .|35  .|34  .|33  .|32  .|31  .|
|30  .|29  .|28  .|27  .|26  .|25  .|24  .|23  .|22  .|21  .|
|20  .|19  .|18  .|17  .|16  .|15  .|14  .|13  a|12  .|11  .|
|10  b|9   .|8   .|7   .|6   .|5   .|4   .|3   .|2   .|1   .|
-------------------------------------------------------------

Player with Id:b moved from position:10 to position:14 after rolling dice with num:4
-------------------------------------------------------------
|100 .|99  .|98  .|97  .|96  .|95  .|94  .|93  .|92  .|91  .|
|90  .|89  .|88  .|87  .|86  .|85  .|84  .|83  .|82  .|81  .|
|80  .|79  .|78  .|77  .|76  .|75  .|74  .|73  .|72  .|71  .|
|70  .|69  .|68  .|67  .|66  .|65  .|64  .|63  .|62  .|61  .|
|60  .|59  .|58  .|57  .|56  .|55  .|54  .|53  .|52  .|51  .|
|50  .|49  .|48  .|47  .|46  .|45  .|44  .|43  .|42  .|41  .|
|40  .|39  .|38  .|37  .|36  .|35  .|34  .|33  .|32  .|31  .|
|30  .|29  .|28  .|27  .|26  .|25  .|24  .|23  .|22  .|21  .|
|20  .|19  .|18  .|17  .|16  .|15  .|14  b|13  a|12  .|11  .|
|10  .|9   .|8   .|7   .|6   .|5   .|4   .|3   .|2   .|1   .|
-------------------------------------------------------------

Player with Id:a moved from position:13 to position:16 after rolling dice with num:3
-------------------------------------------------------------
|100 .|99  .|98  .|97  .|96  .|95  .|94  .|93  .|92  .|91  .|
|90  .|89  .|88  .|87  .|86  .|85  .|84  .|83  .|82  .|81  .|
|80  .|79  .|78  .|77  .|76  .|75  .|74  .|73  .|72  .|71  .|
|70  .|69  .|68  .|67  .|66  .|65  .|64  .|63  .|62  .|61  .|
|60  .|59  .|58  .|57  .|56  .|55  .|54  .|53  .|52  .|51  .|
|50  .|49  .|48  .|47  .|46  .|45  .|44  .|43  .|42  .|41  .|
|40  .|39  .|38  .|37  .|36  .|35  .|34  .|33  .|32  .|31  .|
|30  .|29  .|28  .|27  .|26  .|25  .|24  .|23  .|22  .|21  .|
|20  .|19  .|18  .|17  .|16  a|15  .|14  b|13  .|12  .|11  .|
|10  .|9   .|8   .|7   .|6   .|5   .|4   .|3   .|2   .|1   .|
-------------------------------------------------------------

Player with Id:b moved from position:14 to position:15 after rolling dice with num:1
-------------------------------------------------------------
|100 .|99  .|98  .|97  .|96  .|95  .|94  .|93  .|92  .|91  .|
|90  .|89  .|88  .|87  .|86  .|85  .|84  .|83  .|82  .|81  .|
|80  .|79  .|78  .|77  .|76  .|75  .|74  .|73  .|72  .|71  .|
|70  .|69  .|68  .|67  .|66  .|65  .|64  .|63  .|62  .|61  .|
|60  .|59  .|58  .|57  .|56  .|55  .|54  .|53  .|52  .|51  .|
|50  .|49  .|48  .|47  .|46  .|45  .|44  .|43  .|42  .|41  .|
|40  .|39  .|38  .|37  .|36  .|35  .|34  .|33  .|32  .|31  .|
|30  .|29  .|28  .|27  .|26  .|25  .|24  .|23  .|22  .|21  .|
|20  .|19  .|18  .|17  .|16  a|15  b|14  .|13  .|12  .|11  .|
|10  .|9   .|8   .|7   .|6   .|5   .|4   .|3   .|2   .|1   .|
-------------------------------------------------------------

Player with Id:a moved from position:16 to position:20 after rolling dice with num:4
-------------------------------------------------------------
|100 .|99  .|98  .|97  .|96  .|95  .|94  .|93  .|92  .|91  .|
|90  .|89  .|88  .|87  .|86  .|85  .|84  .|83  .|82  .|81  .|
|80  .|79  .|78  .|77  .|76  .|75  .|74  .|73  .|72  .|71  .|
|70  .|69  .|68  .|67  .|66  .|65  .|64  .|63  .|62  .|61  .|
|60  .|59  .|58  .|57  .|56  .|55  .|54  .|53  .|52  .|51  .|
|50  .|49  .|48  .|47  .|46  .|45  .|44  .|43  .|42  .|41  .|
|40  .|39  .|38  .|37  .|36  .|35  .|34  .|33  .|32  .|31  .|
|30  .|29  .|28  .|27  .|26  .|25  .|24  .|23  .|22  .|21  .|
|20  a|19  .|18  .|17  .|16  .|15  b|14  .|13  .|12  .|11  .|
|10  .|9   .|8   .|7   .|6   .|5   .|4   .|3   .|2   .|1   .|
-------------------------------------------------------------

Player with Id:b moved from position:15 to position:20 after rolling dice with num:5
-------------------------------------------------------------
|100 .|99  .|98  .|97  .|96  .|95  .|94  .|93  .|92  .|91  .|
|90  .|89  .|88  .|87  .|86  .|85  .|84  .|83  .|82  .|81  .|
|80  .|79  .|78  .|77  .|76  .|75  .|74  .|73  .|72  .|71  .|
|70  .|69  .|68  .|67  .|66  .|65  .|64  .|63  .|62  .|61  .|
|60  .|59  .|58  .|57  .|56  .|55  .|54  .|53  .|52  .|51  .|
|50  .|49  .|48  .|47  .|46  .|45  .|44  .|43  .|42  .|41  .|
|40  .|39  .|38  .|37  .|36  .|35  .|34  .|33  .|32  .|31  .|
|30  .|29  .|28  .|27  .|26  .|25  .|24  .|23  .|22  .|21  .|
|20  b,a|19  .|18  .|17  .|16  .|15  .|14  .|13  .|12  .|11  .|
|10  .|9   .|8   .|7   .|6   .|5   .|4   .|3   .|2   .|1   .|
-------------------------------------------------------------

Player with Id:a moved from position:20 to position:23 after rolling dice with num:3
-------------------------------------------------------------
|100 .|99  .|98  .|97  .|96  .|95  .|94  .|93  .|92  .|91  .|
|90  .|89  .|88  .|87  .|86  .|85  .|84  .|83  .|82  .|81  .|
|80  .|79  .|78  .|77  .|76  .|75  .|74  .|73  .|72  .|71  .|
|70  .|69  .|68  .|67  .|66  .|65  .|64  .|63  .|62  .|61  .|
|60  .|59  .|58  .|57  .|56  .|55  .|54  .|53  .|52  .|51  .|
|50  .|49  .|48  .|47  .|46  .|45  .|44  .|43  .|42  .|41  .|
|40  .|39  .|38  .|37  .|36  .|35  .|34  .|33  .|32  .|31  .|
|30  .|29  .|28  .|27  .|26  .|25  .|24  .|23  a|22  .|21  .|
|20  b|19  .|18  .|17  .|16  .|15  .|14  .|13  .|12  .|11  .|
|10  .|9   .|8   .|7   .|6   .|5   .|4   .|3   .|2   .|1   .|
-------------------------------------------------------------

Player with Id:b moved from position:20 to position:22 after rolling dice with num:2
-------------------------------------------------------------
|100 .|99  .|98  .|97  .|96  .|95  .|94  .|93  .|92  .|91  .|
|90  .|89  .|88  .|87  .|86  .|85  .|84  .|83  .|82  .|81  .|
|80  .|79  .|78  .|77  .|76  .|75  .|74  .|73  .|72  .|71  .|
|70  .|69  .|68  .|67  .|66  .|65  .|64  .|63  .|62  .|61  .|
|60  .|59  .|58  .|57  .|56  .|55  .|54  .|53  .|52  .|51  .|
|50  .|49  .|48  .|47  .|46  .|45  .|44  .|43  .|42  .|41  .|
|40  .|39  .|38  .|37  .|36  .|35  .|34  .|33  .|32  .|31  .|
|30  .|29  .|28  .|27  .|26  .|25  .|24  .|23  a|22  b|21  .|
|20  .|19  .|18  .|17  .|16  .|15  .|14  .|13  .|12  .|11  .|
|10  .|9   .|8   .|7   .|6   .|5   .|4   .|3   .|2   .|1   .|
-------------------------------------------------------------

Player with Id:a moved from position:23 to position:28 after rolling dice with num:5
-------------------------------------------------------------
|100 .|99  .|98  .|97  .|96  .|95  .|94  .|93  .|92  .|91  .|
|90  .|89  .|88  .|87  .|86  .|85  .|84  .|83  .|82  .|81  .|
|80  .|79  .|78  .|77  .|76  .|75  .|74  .|73  .|72  .|71  .|
|70  .|69  .|68  .|67  .|66  .|65  .|64  .|63  .|62  .|61  .|
|60  .|59  .|58  .|57  .|56  .|55  .|54  .|53  .|52  .|51  .|
|50  .|49  .|48  .|47  .|46  .|45  .|44  .|43  .|42  .|41  .|
|40  .|39  .|38  .|37  .|36  .|35  .|34  .|33  .|32  .|31  .|
|30  .|29  .|28  a|27  .|26  .|25  .|24  .|23  .|22  b|21  .|
|20  .|19  .|18  .|17  .|16  .|15  .|14  .|13  .|12  .|11  .|
|10  .|9   .|8   .|7   .|6   .|5   .|4   .|3   .|2   .|1   .|
-------------------------------------------------------------

Player with Id:b moved from position:22 to position:27 after rolling dice with num:5
-------------------------------------------------------------
|100 .|99  .|98  .|97  .|96  .|95  .|94  .|93  .|92  .|91  .|
|90  .|89  .|88  .|87  .|86  .|85  .|84  .|83  .|82  .|81  .|
|80  .|79  .|78  .|77  .|76  .|75  .|74  .|73  .|72  .|71  .|
|70  .|69  .|68  .|67  .|66  .|65  .|64  .|63  .|62  .|61  .|
|60  .|59  .|58  .|57  .|56  .|55  .|54  .|53  .|52  .|51  .|
|50  .|49  .|48  .|47  .|46  .|45  .|44  .|43  .|42  .|41  .|
|40  .|39  .|38  .|37  .|36  .|35  .|34  .|33  .|32  .|31  .|
|30  .|29  .|28  a|27  b|26  .|25  .|24  .|23  .|22  .|21  .|
|20  .|19  .|18  .|17  .|16  .|15  .|14  .|13  .|12  .|11  .|
|10  .|9   .|8   .|7   .|6   .|5   .|4   .|3   .|2   .|1   .|
-------------------------------------------------------------

Player with Id:a moved from position:28 to position:32 after rolling dice with num:4
-------------------------------------------------------------
|100 .|99  .|98  .|97  .|96  .|95  .|94  .|93  .|92  .|91  .|
|90  .|89  .|88  .|87  .|86  .|85  .|84  .|83  .|82  .|81  .|
|80  .|79  .|78  .|77  .|76  .|75  .|74  .|73  .|72  .|71  .|
|70  .|69  .|68  .|67  .|66  .|65  .|64  .|63  .|62  .|61  .|
|60  .|59  .|58  .|57  .|56  .|55  .|54  .|53  .|52  .|51  .|
|50  .|49  .|48  .|47  .|46  .|45  .|44  .|43  .|42  .|41  .|
|40  .|39  .|38  .|37  .|36  .|35  .|34  .|33  .|32  a|31  .|
|30  .|29  .|28  .|27  b|26  .|25  .|24  .|23  .|22  .|21  .|
|20  .|19  .|18  .|17  .|16  .|15  .|14  .|13  .|12  .|11  .|
|10  .|9   .|8   .|7   .|6   .|5   .|4   .|3   .|2   .|1   .|
-------------------------------------------------------------

Player with Id:b moved from position:27 to position:33 after rolling dice with num:6
-------------------------------------------------------------
|100 .|99  .|98  .|97  .|96  .|95  .|94  .|93  .|92  .|91  .|
|90  .|89  .|88  .|87  .|86  .|85  .|84  .|83  .|82  .|81  .|
|80  .|79  .|78  .|77  .|76  .|75  .|74  .|73  .|72  .|71  .|
|70  .|69  .|68  .|67  .|66  .|65  .|64  .|63  .|62  .|61  .|
|60  .|59  .|58  .|57  .|56  .|55  .|54  .|53  .|52  .|51  .|
|50  .|49  .|48  .|47  .|46  .|45  .|44  .|43  .|42  .|41  .|
|40  .|39  .|38  .|37  .|36  .|35  .|34  .|33  b|32  a|31  .|
|30  .|29  .|28  .|27  .|26  .|25  .|24  .|23  .|22  .|21  .|
|20  .|19  .|18  .|17  .|16  .|15  .|14  .|13  .|12  .|11  .|
|10  .|9   .|8   .|7   .|6   .|5   .|4   .|3   .|2   .|1   .|
-------------------------------------------------------------

Player with Id:a moved from position:32 to position:35 after rolling dice with num:3 and then to position:25\. Bit by snake, snakeStart:35 snakeEnd:25
-------------------------------------------------------------
|100 .|99  .|98  .|97  .|96  .|95  .|94  .|93  .|92  .|91  .|
|90  .|89  .|88  .|87  .|86  .|85  .|84  .|83  .|82  .|81  .|
|80  .|79  .|78  .|77  .|76  .|75  .|74  .|73  .|72  .|71  .|
|70  .|69  .|68  .|67  .|66  .|65  .|64  .|63  .|62  .|61  .|
|60  .|59  .|58  .|57  .|56  .|55  .|54  .|53  .|52  .|51  .|
|50  .|49  .|48  .|47  .|46  .|45  .|44  .|43  .|42  .|41  .|
|40  .|39  .|38  .|37  .|36  .|35  .|34  .|33  b|32  .|31  .|
|30  .|29  .|28  .|27  .|26  .|25  a|24  .|23  .|22  .|21  .|
|20  .|19  .|18  .|17  .|16  .|15  .|14  .|13  .|12  .|11  .|
|10  .|9   .|8   .|7   .|6   .|5   .|4   .|3   .|2   .|1   .|
-------------------------------------------------------------

Player with Id:b moved from position:33 to position:37 after rolling dice with num:4 and then to position:57\. Climbed by ladder, ladderStart:37 ladderEnd:57
-------------------------------------------------------------
|100 .|99  .|98  .|97  .|96  .|95  .|94  .|93  .|92  .|91  .|
|90  .|89  .|88  .|87  .|86  .|85  .|84  .|83  .|82  .|81  .|
|80  .|79  .|78  .|77  .|76  .|75  .|74  .|73  .|72  .|71  .|
|70  .|69  .|68  .|67  .|66  .|65  .|64  .|63  .|62  .|61  .|
|60  .|59  .|58  .|57  b|56  .|55  .|54  .|53  .|52  .|51  .|
|50  .|49  .|48  .|47  .|46  .|45  .|44  .|43  .|42  .|41  .|
|40  .|39  .|38  .|37  .|36  .|35  .|34  .|33  .|32  .|31  .|
|30  .|29  .|28  .|27  .|26  .|25  a|24  .|23  .|22  .|21  .|
|20  .|19  .|18  .|17  .|16  .|15  .|14  .|13  .|12  .|11  .|
|10  .|9   .|8   .|7   .|6   .|5   .|4   .|3   .|2   .|1   .|
-------------------------------------------------------------

Player with Id:a moved from position:25 to position:29 after rolling dice with num:4
-------------------------------------------------------------
|100 .|99  .|98  .|97  .|96  .|95  .|94  .|93  .|92  .|91  .|
|90  .|89  .|88  .|87  .|86  .|85  .|84  .|83  .|82  .|81  .|
|80  .|79  .|78  .|77  .|76  .|75  .|74  .|73  .|72  .|71  .|
|70  .|69  .|68  .|67  .|66  .|65  .|64  .|63  .|62  .|61  .|
|60  .|59  .|58  .|57  b|56  .|55  .|54  .|53  .|52  .|51  .|
|50  .|49  .|48  .|47  .|46  .|45  .|44  .|43  .|42  .|41  .|
|40  .|39  .|38  .|37  .|36  .|35  .|34  .|33  .|32  .|31  .|
|30  .|29  a|28  .|27  .|26  .|25  .|24  .|23  .|22  .|21  .|
|20  .|19  .|18  .|17  .|16  .|15  .|14  .|13  .|12  .|11  .|
|10  .|9   .|8   .|7   .|6   .|5   .|4   .|3   .|2   .|1   .|
-------------------------------------------------------------

Player with Id:b moved from position:57 to position:61 after rolling dice with num:4
-------------------------------------------------------------
|100 .|99  .|98  .|97  .|96  .|95  .|94  .|93  .|92  .|91  .|
|90  .|89  .|88  .|87  .|86  .|85  .|84  .|83  .|82  .|81  .|
|80  .|79  .|78  .|77  .|76  .|75  .|74  .|73  .|72  .|71  .|
|70  .|69  .|68  .|67  .|66  .|65  .|64  .|63  .|62  .|61  b|
|60  .|59  .|58  .|57  .|56  .|55  .|54  .|53  .|52  .|51  .|
|50  .|49  .|48  .|47  .|46  .|45  .|44  .|43  .|42  .|41  .|
|40  .|39  .|38  .|37  .|36  .|35  .|34  .|33  .|32  .|31  .|
|30  .|29  a|28  .|27  .|26  .|25  .|24  .|23  .|22  .|21  .|
|20  .|19  .|18  .|17  .|16  .|15  .|14  .|13  .|12  .|11  .|
|10  .|9   .|8   .|7   .|6   .|5   .|4   .|3   .|2   .|1   .|
-------------------------------------------------------------

Player with Id:a moved from position:29 to position:34 after rolling dice with num:5
-------------------------------------------------------------
|100 .|99  .|98  .|97  .|96  .|95  .|94  .|93  .|92  .|91  .|
|90  .|89  .|88  .|87  .|86  .|85  .|84  .|83  .|82  .|81  .|
|80  .|79  .|78  .|77  .|76  .|75  .|74  .|73  .|72  .|71  .|
|70  .|69  .|68  .|67  .|66  .|65  .|64  .|63  .|62  .|61  b|
|60  .|59  .|58  .|57  .|56  .|55  .|54  .|53  .|52  .|51  .|
|50  .|49  .|48  .|47  .|46  .|45  .|44  .|43  .|42  .|41  .|
|40  .|39  .|38  .|37  .|36  .|35  .|34  a|33  .|32  .|31  .|
|30  .|29  .|28  .|27  .|26  .|25  .|24  .|23  .|22  .|21  .|
|20  .|19  .|18  .|17  .|16  .|15  .|14  .|13  .|12  .|11  .|
|10  .|9   .|8   .|7   .|6   .|5   .|4   .|3   .|2   .|1   .|
-------------------------------------------------------------

Player with Id:b moved from position:61 to position:63 after rolling dice with num:2
-------------------------------------------------------------
|100 .|99  .|98  .|97  .|96  .|95  .|94  .|93  .|92  .|91  .|
|90  .|89  .|88  .|87  .|86  .|85  .|84  .|83  .|82  .|81  .|
|80  .|79  .|78  .|77  .|76  .|75  .|74  .|73  .|72  .|71  .|
|70  .|69  .|68  .|67  .|66  .|65  .|64  .|63  b|62  .|61  .|
|60  .|59  .|58  .|57  .|56  .|55  .|54  .|53  .|52  .|51  .|
|50  .|49  .|48  .|47  .|46  .|45  .|44  .|43  .|42  .|41  .|
|40  .|39  .|38  .|37  .|36  .|35  .|34  a|33  .|32  .|31  .|
|30  .|29  .|28  .|27  .|26  .|25  .|24  .|23  .|22  .|21  .|
|20  .|19  .|18  .|17  .|16  .|15  .|14  .|13  .|12  .|11  .|
|10  .|9   .|8   .|7   .|6   .|5   .|4   .|3   .|2   .|1   .|
-------------------------------------------------------------

Player with Id:a moved from position:34 to position:39 after rolling dice with num:5
-------------------------------------------------------------
|100 .|99  .|98  .|97  .|96  .|95  .|94  .|93  .|92  .|91  .|
|90  .|89  .|88  .|87  .|86  .|85  .|84  .|83  .|82  .|81  .|
|80  .|79  .|78  .|77  .|76  .|75  .|74  .|73  .|72  .|71  .|
|70  .|69  .|68  .|67  .|66  .|65  .|64  .|63  b|62  .|61  .|
|60  .|59  .|58  .|57  .|56  .|55  .|54  .|53  .|52  .|51  .|
|50  .|49  .|48  .|47  .|46  .|45  .|44  .|43  .|42  .|41  .|
|40  .|39  a|38  .|37  .|36  .|35  .|34  .|33  .|32  .|31  .|
|30  .|29  .|28  .|27  .|26  .|25  .|24  .|23  .|22  .|21  .|
|20  .|19  .|18  .|17  .|16  .|15  .|14  .|13  .|12  .|11  .|
|10  .|9   .|8   .|7   .|6   .|5   .|4   .|3   .|2   .|1   .|
-------------------------------------------------------------

Player with Id:b moved from position:63 to position:64 after rolling dice with num:1
-------------------------------------------------------------
|100 .|99  .|98  .|97  .|96  .|95  .|94  .|93  .|92  .|91  .|
|90  .|89  .|88  .|87  .|86  .|85  .|84  .|83  .|82  .|81  .|
|80  .|79  .|78  .|77  .|76  .|75  .|74  .|73  .|72  .|71  .|
|70  .|69  .|68  .|67  .|66  .|65  .|64  b|63  .|62  .|61  .|
|60  .|59  .|58  .|57  .|56  .|55  .|54  .|53  .|52  .|51  .|
|50  .|49  .|48  .|47  .|46  .|45  .|44  .|43  .|42  .|41  .|
|40  .|39  a|38  .|37  .|36  .|35  .|34  .|33  .|32  .|31  .|
|30  .|29  .|28  .|27  .|26  .|25  .|24  .|23  .|22  .|21  .|
|20  .|19  .|18  .|17  .|16  .|15  .|14  .|13  .|12  .|11  .|
|10  .|9   .|8   .|7   .|6   .|5   .|4   .|3   .|2   .|1   .|
-------------------------------------------------------------

Player with Id:a moved from position:39 to position:44 after rolling dice with num:5
-------------------------------------------------------------
|100 .|99  .|98  .|97  .|96  .|95  .|94  .|93  .|92  .|91  .|
|90  .|89  .|88  .|87  .|86  .|85  .|84  .|83  .|82  .|81  .|
|80  .|79  .|78  .|77  .|76  .|75  .|74  .|73  .|72  .|71  .|
|70  .|69  .|68  .|67  .|66  .|65  .|64  b|63  .|62  .|61  .|
|60  .|59  .|58  .|57  .|56  .|55  .|54  .|53  .|52  .|51  .|
|50  .|49  .|48  .|47  .|46  .|45  .|44  a|43  .|42  .|41  .|
|40  .|39  .|38  .|37  .|36  .|35  .|34  .|33  .|32  .|31  .|
|30  .|29  .|28  .|27  .|26  .|25  .|24  .|23  .|22  .|21  .|
|20  .|19  .|18  .|17  .|16  .|15  .|14  .|13  .|12  .|11  .|
|10  .|9   .|8   .|7   .|6   .|5   .|4   .|3   .|2   .|1   .|
-------------------------------------------------------------

Player with Id:b moved from position:64 to position:69 after rolling dice with num:5
-------------------------------------------------------------
|100 .|99  .|98  .|97  .|96  .|95  .|94  .|93  .|92  .|91  .|
|90  .|89  .|88  .|87  .|86  .|85  .|84  .|83  .|82  .|81  .|
|80  .|79  .|78  .|77  .|76  .|75  .|74  .|73  .|72  .|71  .|
|70  .|69  b|68  .|67  .|66  .|65  .|64  .|63  .|62  .|61  .|
|60  .|59  .|58  .|57  .|56  .|55  .|54  .|53  .|52  .|51  .|
|50  .|49  .|48  .|47  .|46  .|45  .|44  a|43  .|42  .|41  .|
|40  .|39  .|38  .|37  .|36  .|35  .|34  .|33  .|32  .|31  .|
|30  .|29  .|28  .|27  .|26  .|25  .|24  .|23  .|22  .|21  .|
|20  .|19  .|18  .|17  .|16  .|15  .|14  .|13  .|12  .|11  .|
|10  .|9   .|8   .|7   .|6   .|5   .|4   .|3   .|2   .|1   .|
-------------------------------------------------------------

Player with Id:a moved from position:44 to position:46 after rolling dice with num:2
-------------------------------------------------------------
|100 .|99  .|98  .|97  .|96  .|95  .|94  .|93  .|92  .|91  .|
|90  .|89  .|88  .|87  .|86  .|85  .|84  .|83  .|82  .|81  .|
|80  .|79  .|78  .|77  .|76  .|75  .|74  .|73  .|72  .|71  .|
|70  .|69  b|68  .|67  .|66  .|65  .|64  .|63  .|62  .|61  .|
|60  .|59  .|58  .|57  .|56  .|55  .|54  .|53  .|52  .|51  .|
|50  .|49  .|48  .|47  .|46  a|45  .|44  .|43  .|42  .|41  .|
|40  .|39  .|38  .|37  .|36  .|35  .|34  .|33  .|32  .|31  .|
|30  .|29  .|28  .|27  .|26  .|25  .|24  .|23  .|22  .|21  .|
|20  .|19  .|18  .|17  .|16  .|15  .|14  .|13  .|12  .|11  .|
|10  .|9   .|8   .|7   .|6   .|5   .|4   .|3   .|2   .|1   .|
-------------------------------------------------------------

Player with Id:b moved from position:69 to position:70 after rolling dice with num:1
-------------------------------------------------------------
|100 .|99  .|98  .|97  .|96  .|95  .|94  .|93  .|92  .|91  .|
|90  .|89  .|88  .|87  .|86  .|85  .|84  .|83  .|82  .|81  .|
|80  .|79  .|78  .|77  .|76  .|75  .|74  .|73  .|72  .|71  .|
|70  b|69  .|68  .|67  .|66  .|65  .|64  .|63  .|62  .|61  .|
|60  .|59  .|58  .|57  .|56  .|55  .|54  .|53  .|52  .|51  .|
|50  .|49  .|48  .|47  .|46  a|45  .|44  .|43  .|42  .|41  .|
|40  .|39  .|38  .|37  .|36  .|35  .|34  .|33  .|32  .|31  .|
|30  .|29  .|28  .|27  .|26  .|25  .|24  .|23  .|22  .|21  .|
|20  .|19  .|18  .|17  .|16  .|15  .|14  .|13  .|12  .|11  .|
|10  .|9   .|8   .|7   .|6   .|5   .|4   .|3   .|2   .|1   .|
-------------------------------------------------------------

Player with Id:a moved from position:46 to position:48 after rolling dice with num:2
-------------------------------------------------------------
|100 .|99  .|98  .|97  .|96  .|95  .|94  .|93  .|92  .|91  .|
|90  .|89  .|88  .|87  .|86  .|85  .|84  .|83  .|82  .|81  .|
|80  .|79  .|78  .|77  .|76  .|75  .|74  .|73  .|72  .|71  .|
|70  b|69  .|68  .|67  .|66  .|65  .|64  .|63  .|62  .|61  .|
|60  .|59  .|58  .|57  .|56  .|55  .|54  .|53  .|52  .|51  .|
|50  .|49  .|48  a|47  .|46  .|45  .|44  .|43  .|42  .|41  .|
|40  .|39  .|38  .|37  .|36  .|35  .|34  .|33  .|32  .|31  .|
|30  .|29  .|28  .|27  .|26  .|25  .|24  .|23  .|22  .|21  .|
|20  .|19  .|18  .|17  .|16  .|15  .|14  .|13  .|12  .|11  .|
|10  .|9   .|8   .|7   .|6   .|5   .|4   .|3   .|2   .|1   .|
-------------------------------------------------------------

Player with Id:b moved from position:70 to position:71 after rolling dice with num:1
-------------------------------------------------------------
|100 .|99  .|98  .|97  .|96  .|95  .|94  .|93  .|92  .|91  .|
|90  .|89  .|88  .|87  .|86  .|85  .|84  .|83  .|82  .|81  .|
|80  .|79  .|78  .|77  .|76  .|75  .|74  .|73  .|72  .|71  b|
|70  .|69  .|68  .|67  .|66  .|65  .|64  .|63  .|62  .|61  .|
|60  .|59  .|58  .|57  .|56  .|55  .|54  .|53  .|52  .|51  .|
|50  .|49  .|48  a|47  .|46  .|45  .|44  .|43  .|42  .|41  .|
|40  .|39  .|38  .|37  .|36  .|35  .|34  .|33  .|32  .|31  .|
|30  .|29  .|28  .|27  .|26  .|25  .|24  .|23  .|22  .|21  .|
|20  .|19  .|18  .|17  .|16  .|15  .|14  .|13  .|12  .|11  .|
|10  .|9   .|8   .|7   .|6   .|5   .|4   .|3   .|2   .|1   .|
-------------------------------------------------------------

Player with Id:a moved from position:48 to position:50 after rolling dice with num:2
-------------------------------------------------------------
|100 .|99  .|98  .|97  .|96  .|95  .|94  .|93  .|92  .|91  .|
|90  .|89  .|88  .|87  .|86  .|85  .|84  .|83  .|82  .|81  .|
|80  .|79  .|78  .|77  .|76  .|75  .|74  .|73  .|72  .|71  b|
|70  .|69  .|68  .|67  .|66  .|65  .|64  .|63  .|62  .|61  .|
|60  .|59  .|58  .|57  .|56  .|55  .|54  .|53  .|52  .|51  .|
|50  a|49  .|48  .|47  .|46  .|45  .|44  .|43  .|42  .|41  .|
|40  .|39  .|38  .|37  .|36  .|35  .|34  .|33  .|32  .|31  .|
|30  .|29  .|28  .|27  .|26  .|25  .|24  .|23  .|22  .|21  .|
|20  .|19  .|18  .|17  .|16  .|15  .|14  .|13  .|12  .|11  .|
|10  .|9   .|8   .|7   .|6   .|5   .|4   .|3   .|2   .|1   .|
-------------------------------------------------------------

Player with Id:b moved from position:71 to position:75 after rolling dice with num:4
-------------------------------------------------------------
|100 .|99  .|98  .|97  .|96  .|95  .|94  .|93  .|92  .|91  .|
|90  .|89  .|88  .|87  .|86  .|85  .|84  .|83  .|82  .|81  .|
|80  .|79  .|78  .|77  .|76  .|75  b|74  .|73  .|72  .|71  .|
|70  .|69  .|68  .|67  .|66  .|65  .|64  .|63  .|62  .|61  .|
|60  .|59  .|58  .|57  .|56  .|55  .|54  .|53  .|52  .|51  .|
|50  a|49  .|48  .|47  .|46  .|45  .|44  .|43  .|42  .|41  .|
|40  .|39  .|38  .|37  .|36  .|35  .|34  .|33  .|32  .|31  .|
|30  .|29  .|28  .|27  .|26  .|25  .|24  .|23  .|22  .|21  .|
|20  .|19  .|18  .|17  .|16  .|15  .|14  .|13  .|12  .|11  .|
|10  .|9   .|8   .|7   .|6   .|5   .|4   .|3   .|2   .|1   .|
-------------------------------------------------------------

Player with Id:a moved from position:50 to position:55 after rolling dice with num:5
-------------------------------------------------------------
|100 .|99  .|98  .|97  .|96  .|95  .|94  .|93  .|92  .|91  .|
|90  .|89  .|88  .|87  .|86  .|85  .|84  .|83  .|82  .|81  .|
|80  .|79  .|78  .|77  .|76  .|75  b|74  .|73  .|72  .|71  .|
|70  .|69  .|68  .|67  .|66  .|65  .|64  .|63  .|62  .|61  .|
|60  .|59  .|58  .|57  .|56  .|55  a|54  .|53  .|52  .|51  .|
|50  .|49  .|48  .|47  .|46  .|45  .|44  .|43  .|42  .|41  .|
|40  .|39  .|38  .|37  .|36  .|35  .|34  .|33  .|32  .|31  .|
|30  .|29  .|28  .|27  .|26  .|25  .|24  .|23  .|22  .|21  .|
|20  .|19  .|18  .|17  .|16  .|15  .|14  .|13  .|12  .|11  .|
|10  .|9   .|8   .|7   .|6   .|5   .|4   .|3   .|2   .|1   .|
-------------------------------------------------------------

Player with Id:b moved from position:75 to position:79 after rolling dice with num:4
-------------------------------------------------------------
|100 .|99  .|98  .|97  .|96  .|95  .|94  .|93  .|92  .|91  .|
|90  .|89  .|88  .|87  .|86  .|85  .|84  .|83  .|82  .|81  .|
|80  .|79  b|78  .|77  .|76  .|75  .|74  .|73  .|72  .|71  .|
|70  .|69  .|68  .|67  .|66  .|65  .|64  .|63  .|62  .|61  .|
|60  .|59  .|58  .|57  .|56  .|55  a|54  .|53  .|52  .|51  .|
|50  .|49  .|48  .|47  .|46  .|45  .|44  .|43  .|42  .|41  .|
|40  .|39  .|38  .|37  .|36  .|35  .|34  .|33  .|32  .|31  .|
|30  .|29  .|28  .|27  .|26  .|25  .|24  .|23  .|22  .|21  .|
|20  .|19  .|18  .|17  .|16  .|15  .|14  .|13  .|12  .|11  .|
|10  .|9   .|8   .|7   .|6   .|5   .|4   .|3   .|2   .|1   .|
-------------------------------------------------------------

Player with Id:a moved from position:55 to position:60 after rolling dice with num:5
-------------------------------------------------------------
|100 .|99  .|98  .|97  .|96  .|95  .|94  .|93  .|92  .|91  .|
|90  .|89  .|88  .|87  .|86  .|85  .|84  .|83  .|82  .|81  .|
|80  .|79  b|78  .|77  .|76  .|75  .|74  .|73  .|72  .|71  .|
|70  .|69  .|68  .|67  .|66  .|65  .|64  .|63  .|62  .|61  .|
|60  a|59  .|58  .|57  .|56  .|55  .|54  .|53  .|52  .|51  .|
|50  .|49  .|48  .|47  .|46  .|45  .|44  .|43  .|42  .|41  .|
|40  .|39  .|38  .|37  .|36  .|35  .|34  .|33  .|32  .|31  .|
|30  .|29  .|28  .|27  .|26  .|25  .|24  .|23  .|22  .|21  .|
|20  .|19  .|18  .|17  .|16  .|15  .|14  .|13  .|12  .|11  .|
|10  .|9   .|8   .|7   .|6   .|5   .|4   .|3   .|2   .|1   .|
-------------------------------------------------------------

Player with Id:b moved from position:79 to position:83 after rolling dice with num:4
-------------------------------------------------------------
|100 .|99  .|98  .|97  .|96  .|95  .|94  .|93  .|92  .|91  .|
|90  .|89  .|88  .|87  .|86  .|85  .|84  .|83  b|82  .|81  .|
|80  .|79  .|78  .|77  .|76  .|75  .|74  .|73  .|72  .|71  .|
|70  .|69  .|68  .|67  .|66  .|65  .|64  .|63  .|62  .|61  .|
|60  a|59  .|58  .|57  .|56  .|55  .|54  .|53  .|52  .|51  .|
|50  .|49  .|48  .|47  .|46  .|45  .|44  .|43  .|42  .|41  .|
|40  .|39  .|38  .|37  .|36  .|35  .|34  .|33  .|32  .|31  .|
|30  .|29  .|28  .|27  .|26  .|25  .|24  .|23  .|22  .|21  .|
|20  .|19  .|18  .|17  .|16  .|15  .|14  .|13  .|12  .|11  .|
|10  .|9   .|8   .|7   .|6   .|5   .|4   .|3   .|2   .|1   .|
-------------------------------------------------------------

Player with Id:a moved from position:60 to position:64 after rolling dice with num:4
-------------------------------------------------------------
|100 .|99  .|98  .|97  .|96  .|95  .|94  .|93  .|92  .|91  .|
|90  .|89  .|88  .|87  .|86  .|85  .|84  .|83  b|82  .|81  .|
|80  .|79  .|78  .|77  .|76  .|75  .|74  .|73  .|72  .|71  .|
|70  .|69  .|68  .|67  .|66  .|65  .|64  a|63  .|62  .|61  .|
|60  .|59  .|58  .|57  .|56  .|55  .|54  .|53  .|52  .|51  .|
|50  .|49  .|48  .|47  .|46  .|45  .|44  .|43  .|42  .|41  .|
|40  .|39  .|38  .|37  .|36  .|35  .|34  .|33  .|32  .|31  .|
|30  .|29  .|28  .|27  .|26  .|25  .|24  .|23  .|22  .|21  .|
|20  .|19  .|18  .|17  .|16  .|15  .|14  .|13  .|12  .|11  .|
|10  .|9   .|8   .|7   .|6   .|5   .|4   .|3   .|2   .|1   .|
-------------------------------------------------------------

Player with Id:b moved from position:83 to position:86 after rolling dice with num:3
-------------------------------------------------------------
|100 .|99  .|98  .|97  .|96  .|95  .|94  .|93  .|92  .|91  .|
|90  .|89  .|88  .|87  .|86  b|85  .|84  .|83  .|82  .|81  .|
|80  .|79  .|78  .|77  .|76  .|75  .|74  .|73  .|72  .|71  .|
|70  .|69  .|68  .|67  .|66  .|65  .|64  a|63  .|62  .|61  .|
|60  .|59  .|58  .|57  .|56  .|55  .|54  .|53  .|52  .|51  .|
|50  .|49  .|48  .|47  .|46  .|45  .|44  .|43  .|42  .|41  .|
|40  .|39  .|38  .|37  .|36  .|35  .|34  .|33  .|32  .|31  .|
|30  .|29  .|28  .|27  .|26  .|25  .|24  .|23  .|22  .|21  .|
|20  .|19  .|18  .|17  .|16  .|15  .|14  .|13  .|12  .|11  .|
|10  .|9   .|8   .|7   .|6   .|5   .|4   .|3   .|2   .|1   .|
-------------------------------------------------------------

Player with Id:a moved from position:64 to position:66 after rolling dice with num:2
-------------------------------------------------------------
|100 .|99  .|98  .|97  .|96  .|95  .|94  .|93  .|92  .|91  .|
|90  .|89  .|88  .|87  .|86  b|85  .|84  .|83  .|82  .|81  .|
|80  .|79  .|78  .|77  .|76  .|75  .|74  .|73  .|72  .|71  .|
|70  .|69  .|68  .|67  .|66  a|65  .|64  .|63  .|62  .|61  .|
|60  .|59  .|58  .|57  .|56  .|55  .|54  .|53  .|52  .|51  .|
|50  .|49  .|48  .|47  .|46  .|45  .|44  .|43  .|42  .|41  .|
|40  .|39  .|38  .|37  .|36  .|35  .|34  .|33  .|32  .|31  .|
|30  .|29  .|28  .|27  .|26  .|25  .|24  .|23  .|22  .|21  .|
|20  .|19  .|18  .|17  .|16  .|15  .|14  .|13  .|12  .|11  .|
|10  .|9   .|8   .|7   .|6   .|5   .|4   .|3   .|2   .|1   .|
-------------------------------------------------------------

Player with Id:b moved from position:86 to position:88 after rolling dice with num:2
-------------------------------------------------------------
|100 .|99  .|98  .|97  .|96  .|95  .|94  .|93  .|92  .|91  .|
|90  .|89  .|88  b|87  .|86  .|85  .|84  .|83  .|82  .|81  .|
|80  .|79  .|78  .|77  .|76  .|75  .|74  .|73  .|72  .|71  .|
|70  .|69  .|68  .|67  .|66  a|65  .|64  .|63  .|62  .|61  .|
|60  .|59  .|58  .|57  .|56  .|55  .|54  .|53  .|52  .|51  .|
|50  .|49  .|48  .|47  .|46  .|45  .|44  .|43  .|42  .|41  .|
|40  .|39  .|38  .|37  .|36  .|35  .|34  .|33  .|32  .|31  .|
|30  .|29  .|28  .|27  .|26  .|25  .|24  .|23  .|22  .|21  .|
|20  .|19  .|18  .|17  .|16  .|15  .|14  .|13  .|12  .|11  .|
|10  .|9   .|8   .|7   .|6   .|5   .|4   .|3   .|2   .|1   .|
-------------------------------------------------------------

Player with Id:a moved from position:66 to position:72 after rolling dice with num:6
-------------------------------------------------------------
|100 .|99  .|98  .|97  .|96  .|95  .|94  .|93  .|92  .|91  .|
|90  .|89  .|88  b|87  .|86  .|85  .|84  .|83  .|82  .|81  .|
|80  .|79  .|78  .|77  .|76  .|75  .|74  .|73  .|72  a|71  .|
|70  .|69  .|68  .|67  .|66  .|65  .|64  .|63  .|62  .|61  .|
|60  .|59  .|58  .|57  .|56  .|55  .|54  .|53  .|52  .|51  .|
|50  .|49  .|48  .|47  .|46  .|45  .|44  .|43  .|42  .|41  .|
|40  .|39  .|38  .|37  .|36  .|35  .|34  .|33  .|32  .|31  .|
|30  .|29  .|28  .|27  .|26  .|25  .|24  .|23  .|22  .|21  .|
|20  .|19  .|18  .|17  .|16  .|15  .|14  .|13  .|12  .|11  .|
|10  .|9   .|8   .|7   .|6   .|5   .|4   .|3   .|2   .|1   .|
-------------------------------------------------------------

Player with Id:b moved from position:88 to position:90 after rolling dice with num:2
-------------------------------------------------------------
|100 .|99  .|98  .|97  .|96  .|95  .|94  .|93  .|92  .|91  .|
|90  b|89  .|88  .|87  .|86  .|85  .|84  .|83  .|82  .|81  .|
|80  .|79  .|78  .|77  .|76  .|75  .|74  .|73  .|72  a|71  .|
|70  .|69  .|68  .|67  .|66  .|65  .|64  .|63  .|62  .|61  .|
|60  .|59  .|58  .|57  .|56  .|55  .|54  .|53  .|52  .|51  .|
|50  .|49  .|48  .|47  .|46  .|45  .|44  .|43  .|42  .|41  .|
|40  .|39  .|38  .|37  .|36  .|35  .|34  .|33  .|32  .|31  .|
|30  .|29  .|28  .|27  .|26  .|25  .|24  .|23  .|22  .|21  .|
|20  .|19  .|18  .|17  .|16  .|15  .|14  .|13  .|12  .|11  .|
|10  .|9   .|8   .|7   .|6   .|5   .|4   .|3   .|2   .|1   .|
-------------------------------------------------------------

Player with Id:a moved from position:72 to position:76 after rolling dice with num:4
-------------------------------------------------------------
|100 .|99  .|98  .|97  .|96  .|95  .|94  .|93  .|92  .|91  .|
|90  b|89  .|88  .|87  .|86  .|85  .|84  .|83  .|82  .|81  .|
|80  .|79  .|78  .|77  .|76  a|75  .|74  .|73  .|72  .|71  .|
|70  .|69  .|68  .|67  .|66  .|65  .|64  .|63  .|62  .|61  .|
|60  .|59  .|58  .|57  .|56  .|55  .|54  .|53  .|52  .|51  .|
|50  .|49  .|48  .|47  .|46  .|45  .|44  .|43  .|42  .|41  .|
|40  .|39  .|38  .|37  .|36  .|35  .|34  .|33  .|32  .|31  .|
|30  .|29  .|28  .|27  .|26  .|25  .|24  .|23  .|22  .|21  .|
|20  .|19  .|18  .|17  .|16  .|15  .|14  .|13  .|12  .|11  .|
|10  .|9   .|8   .|7   .|6   .|5   .|4   .|3   .|2   .|1   .|
-------------------------------------------------------------

Player with Id:b moved from position:90 to position:93 after rolling dice with num:3
-------------------------------------------------------------
|100 .|99  .|98  .|97  .|96  .|95  .|94  .|93  b|92  .|91  .|
|90  .|89  .|88  .|87  .|86  .|85  .|84  .|83  .|82  .|81  .|
|80  .|79  .|78  .|77  .|76  a|75  .|74  .|73  .|72  .|71  .|
|70  .|69  .|68  .|67  .|66  .|65  .|64  .|63  .|62  .|61  .|
|60  .|59  .|58  .|57  .|56  .|55  .|54  .|53  .|52  .|51  .|
|50  .|49  .|48  .|47  .|46  .|45  .|44  .|43  .|42  .|41  .|
|40  .|39  .|38  .|37  .|36  .|35  .|34  .|33  .|32  .|31  .|
|30  .|29  .|28  .|27  .|26  .|25  .|24  .|23  .|22  .|21  .|
|20  .|19  .|18  .|17  .|16  .|15  .|14  .|13  .|12  .|11  .|
|10  .|9   .|8   .|7   .|6   .|5   .|4   .|3   .|2   .|1   .|
-------------------------------------------------------------

Player with Id:a moved from position:76 to position:80 after rolling dice with num:4
-------------------------------------------------------------
|100 .|99  .|98  .|97  .|96  .|95  .|94  .|93  b|92  .|91  .|
|90  .|89  .|88  .|87  .|86  .|85  .|84  .|83  .|82  .|81  .|
|80  a|79  .|78  .|77  .|76  .|75  .|74  .|73  .|72  .|71  .|
|70  .|69  .|68  .|67  .|66  .|65  .|64  .|63  .|62  .|61  .|
|60  .|59  .|58  .|57  .|56  .|55  .|54  .|53  .|52  .|51  .|
|50  .|49  .|48  .|47  .|46  .|45  .|44  .|43  .|42  .|41  .|
|40  .|39  .|38  .|37  .|36  .|35  .|34  .|33  .|32  .|31  .|
|30  .|29  .|28  .|27  .|26  .|25  .|24  .|23  .|22  .|21  .|
|20  .|19  .|18  .|17  .|16  .|15  .|14  .|13  .|12  .|11  .|
|10  .|9   .|8   .|7   .|6   .|5   .|4   .|3   .|2   .|1   .|
-------------------------------------------------------------

Player with Id:b moved from position:93 to position:97 after rolling dice with num:4
-------------------------------------------------------------
|100 .|99  .|98  .|97  b|96  .|95  .|94  .|93  .|92  .|91  .|
|90  .|89  .|88  .|87  .|86  .|85  .|84  .|83  .|82  .|81  .|
|80  a|79  .|78  .|77  .|76  .|75  .|74  .|73  .|72  .|71  .|
|70  .|69  .|68  .|67  .|66  .|65  .|64  .|63  .|62  .|61  .|
|60  .|59  .|58  .|57  .|56  .|55  .|54  .|53  .|52  .|51  .|
|50  .|49  .|48  .|47  .|46  .|45  .|44  .|43  .|42  .|41  .|
|40  .|39  .|38  .|37  .|36  .|35  .|34  .|33  .|32  .|31  .|
|30  .|29  .|28  .|27  .|26  .|25  .|24  .|23  .|22  .|21  .|
|20  .|19  .|18  .|17  .|16  .|15  .|14  .|13  .|12  .|11  .|
|10  .|9   .|8   .|7   .|6   .|5   .|4   .|3   .|2   .|1   .|
-------------------------------------------------------------

Player with Id:a moved from position:80 to position:86 after rolling dice with num:6
-------------------------------------------------------------
|100 .|99  .|98  .|97  b|96  .|95  .|94  .|93  .|92  .|91  .|
|90  .|89  .|88  .|87  .|86  a|85  .|84  .|83  .|82  .|81  .|
|80  .|79  .|78  .|77  .|76  .|75  .|74  .|73  .|72  .|71  .|
|70  .|69  .|68  .|67  .|66  .|65  .|64  .|63  .|62  .|61  .|
|60  .|59  .|58  .|57  .|56  .|55  .|54  .|53  .|52  .|51  .|
|50  .|49  .|48  .|47  .|46  .|45  .|44  .|43  .|42  .|41  .|
|40  .|39  .|38  .|37  .|36  .|35  .|34  .|33  .|32  .|31  .|
|30  .|29  .|28  .|27  .|26  .|25  .|24  .|23  .|22  .|21  .|
|20  .|19  .|18  .|17  .|16  .|15  .|14  .|13  .|12  .|11  .|
|10  .|9   .|8   .|7   .|6   .|5   .|4   .|3   .|2   .|1   .|
-------------------------------------------------------------

Player with Id:b moved from position:97 to position:99 after rolling dice with num:2
-------------------------------------------------------------
|100 .|99  b|98  .|97  .|96  .|95  .|94  .|93  .|92  .|91  .|
|90  .|89  .|88  .|87  .|86  a|85  .|84  .|83  .|82  .|81  .|
|80  .|79  .|78  .|77  .|76  .|75  .|74  .|73  .|72  .|71  .|
|70  .|69  .|68  .|67  .|66  .|65  .|64  .|63  .|62  .|61  .|
|60  .|59  .|58  .|57  .|56  .|55  .|54  .|53  .|52  .|51  .|
|50  .|49  .|48  .|47  .|46  .|45  .|44  .|43  .|42  .|41  .|
|40  .|39  .|38  .|37  .|36  .|35  .|34  .|33  .|32  .|31  .|
|30  .|29  .|28  .|27  .|26  .|25  .|24  .|23  .|22  .|21  .|
|20  .|19  .|18  .|17  .|16  .|15  .|14  .|13  .|12  .|11  .|
|10  .|9   .|8   .|7   .|6   .|5   .|4   .|3   .|2   .|1   .|
-------------------------------------------------------------

Player with Id:a moved from position:86 to position:87 after rolling dice with num:1
-------------------------------------------------------------
|100 .|99  b|98  .|97  .|96  .|95  .|94  .|93  .|92  .|91  .|
|90  .|89  .|88  .|87  a|86  .|85  .|84  .|83  .|82  .|81  .|
|80  .|79  .|78  .|77  .|76  .|75  .|74  .|73  .|72  .|71  .|
|70  .|69  .|68  .|67  .|66  .|65  .|64  .|63  .|62  .|61  .|
|60  .|59  .|58  .|57  .|56  .|55  .|54  .|53  .|52  .|51  .|
|50  .|49  .|48  .|47  .|46  .|45  .|44  .|43  .|42  .|41  .|
|40  .|39  .|38  .|37  .|36  .|35  .|34  .|33  .|32  .|31  .|
|30  .|29  .|28  .|27  .|26  .|25  .|24  .|23  .|22  .|21  .|
|20  .|19  .|18  .|17  .|16  .|15  .|14  .|13  .|12  .|11  .|
|10  .|9   .|8   .|7   .|6   .|5   .|4   .|3   .|2   .|1   .|
-------------------------------------------------------------

Player with Id:b moved from position:99 to position:100 after rolling dice with num:1
Game Finished
Players with ID:b won
-------------------------------------------------------------
|100 b|99  .|98  .|97  .|96  .|95  .|94  .|93  .|92  .|91  .|
|90  .|89  .|88  .|87  a|86  .|85  .|84  .|83  .|82  .|81  .|
|80  .|79  .|78  .|77  .|76  .|75  .|74  .|73  .|72  .|71  .|
|70  .|69  .|68  .|67  .|66  .|65  .|64  .|63  .|62  .|61  .|
|60  .|59  .|58  .|57  .|56  .|55  .|54  .|53  .|52  .|51  .|
|50  .|49  .|48  .|47  .|46  .|45  .|44  .|43  .|42  .|41  .|
|40  .|39  .|38  .|37  .|36  .|35  .|34  .|33  .|32  .|31  .|
|30  .|29  .|28  .|27  .|26  .|25  .|24  .|23  .|22  .|21  .|
|20  .|19  .|18  .|17  .|16  .|15  .|14  .|13  .|12  .|11  .|
|10  .|9   .|8   .|7   .|6   .|5   .|4   .|3   .|2   .|1   .|
-------------------------------------------------------------

完整工作代码在一个文件中

这里是完整的工作代码,全部在一个文件中

package main

import (
	"fmt"
	"math/rand"
	"strconv"
	"strings"
	"time"
)

type iPlayer interface {
	rollDice(numDice int) (diceValue int)
	getID() (playerId string)
	setPosition(newPosition int)
	getPosition() (currPosition int)
}

type humanPlayer struct {
	position int
	id       string
}

func (h *humanPlayer) rollDice(numDice int) int {
	diceValue := 0

	for i := 0; i < numDice; i++ {
		rand.Seed(time.Now().UnixNano())
		diceValue += rand.Intn(6) + 1
	}
	return diceValue
}

func (h *humanPlayer) getID() string {
	return h.id
}

func (h *humanPlayer) setPosition(newPosition int) {
	h.position = newPosition
}

func (h *humanPlayer) getPosition() int {
	return h.position
}

type computerPlayer struct {
	position int
	id       string
}

func (h *computerPlayer) rollDice(numDice int) int {
	diceValue := 0

	for i := 0; i < numDice; i++ {
		rand.Seed(time.Now().UnixNano())
		diceValue += rand.Intn(6) + 1
	}
	return diceValue
}

func (h *computerPlayer) getID() string {
	return h.id
}

func (h *computerPlayer) setPosition(newPosition int) {
	h.position = newPosition
}

func (h *computerPlayer) getPosition() int {
	return h.position
}

type GameStatus uint8

const (
	GameInProgress GameStatus = iota
	GameFinished
)

type Symbol struct {
	playersIdMap map[string]bool
}

func (s Symbol) getRepresentation(x, y, dimension int) string {
	val := ""
	playerIds := make([]string, 0)
	for k, v := range s.playersIdMap {
		if v {
			playerIds = append(playerIds, k)
		}
	}
	if len(playerIds) > 0 {
		val = strings.Join(playerIds, ",")
	} else {
		val = "."
	}
	num := x*dimension + y + 1
	strr := strconv.Itoa(num)
	rem := 3 - len(strr)
	for i := 0; i < rem; i++ {
		strr = strr + " "
	}
	strr = strr + " " + val + "|"
	return strr
}

type snake struct {
	start int
	end   int
}

func createSnake(start, end int) (snake, error) {
	if end >= start {
		return snake{}, fmt.Errorf("End cannot be greater than Start for a snake")
	}

	snake := snake{
		start: start,
		end:   end,
	}

	return snake, nil
}

type ladder struct {
	start int
	end   int
}

func createLadder(start, end int) (ladder, error) {
	if start >= end {
		return ladder{}, fmt.Errorf("Start cannot be greater than End for a ladder")
	}

	ladder := ladder{
		start: start,
		end:   end,
	}

	return ladder, nil
}

type board struct {
	square    [][]Symbol
	dimension int
	snakeMap  map[int]int
	ladderMap map[int]int
}

func createBoard(snakes []snake, ladders []ladder, dimension int, players []iPlayer) *board {
	snakeMap := make(map[int]int)
	ladderMap := make(map[int]int)

	for i := 0; i < len(snakes); i++ {
		snakeMap[snakes[i].start] = snakes[i].end
	}

	for i := 0; i < len(ladders); i++ {
		ladderMap[ladders[i].start] = ladders[i].end
	}

	square := make([][]Symbol, dimension)
	for i := 0; i < dimension; i++ {
		square[i] = make([]Symbol, dimension)
	}

	for i := 0; i < dimension; i++ {
		for j := 0; j < dimension; j++ {
			square[i][j] = Symbol{
				playersIdMap: make(map[string]bool),
			}
		}
	}

	playersIdMap := make(map[string]bool)
	for i := 0; i < len(players); i++ {
		player := players[i]
		playersIdMap[player.getID()] = true
	}
	square[0][0] = Symbol{
		playersIdMap: playersIdMap,
	}

	return &board{
		square:    square,
		dimension: dimension,
		snakeMap:  snakeMap,
		ladderMap: ladderMap,
	}
}

func (b *board) markPosition(player iPlayer, currPosition, newPosition int) {
	player.setPosition(newPosition)
	b.removePlayerFromPosition(player, currPosition)
	b.addPlayerToPosition(player, newPosition)
}

func (b *board) removePlayerFromPosition(player iPlayer, position int) {
	x := (position - 1) / b.dimension
	y := (position - 1) % b.dimension
	currSymbol := b.square[x][y]
	delete(currSymbol.playersIdMap, player.getID())
	b.square[x][y] = currSymbol
}

func (b *board) addPlayerToPosition(player iPlayer, position int) {
	x := (position - 1) / b.dimension
	y := (position - 1) % b.dimension
	currSymbol := b.square[x][y]
	currSymbol.playersIdMap[player.getID()] = true
	b.square[x][y] = currSymbol
}

func (b *board) makeMove(player iPlayer, diceNum int) (playerWon bool) {
	currPosition := player.getPosition()
	newPosition := currPosition + diceNum

	if newPosition == b.dimension*b.dimension {
		b.printMoveNormal(player, currPosition, newPosition, diceNum)
		b.markPosition(player, currPosition, newPosition)
		playerWon = true
		return playerWon
	}

	if newPosition > b.dimension*b.dimension {
		newPosition = currPosition
		b.printMoveNormal(player, currPosition, newPosition, diceNum)
		b.markPosition(player, currPosition, newPosition)
		return false
	}

	end, ok := b.snakeMap[newPosition]

	if ok {
		snakeBitPosition := newPosition
		newPosition = end
		b.markPosition(player, currPosition, newPosition)
		b.printMoveSnake(player, currPosition, snakeBitPosition, diceNum, snakeBitPosition, b.snakeMap[snakeBitPosition])
		return false
	}

	end, ok = b.ladderMap[newPosition]

	if ok {
		ladderClimbPosition := newPosition
		newPosition = end
		b.markPosition(player, currPosition, newPosition)
		b.printMoveLadder(player, currPosition, ladderClimbPosition, diceNum, ladderClimbPosition, b.ladderMap[ladderClimbPosition])
		return false
	}

	b.printMoveNormal(player, currPosition, newPosition, diceNum)
	b.markPosition(player, currPosition, newPosition)
	player.setPosition(newPosition)
	return false
}

func (g *board) printMoveNormal(player iPlayer, currPosition, newPosition, diceNum int) {
	fmt.Printf("Player with Id:%s moved from position:%d to position:%d after rolling dice with num:%d\n", player.getID(), currPosition, newPosition, diceNum)
}

func (g *board) printMoveSnake(player iPlayer, currPosition, snakeBitPosition, diceNum, snakeStart, snakeEnd int) {
	fmt.Printf("Player with Id:%s moved from position:%d to position:%d after rolling dice with num:%d and then to position:%d. Bit by snake, snakeStart:%d snakeEnd:%d\n", player.getID(), currPosition, snakeBitPosition, diceNum, snakeEnd, snakeStart, snakeEnd)
}

func (g *board) printMoveLadder(player iPlayer, currPosition, ladderClimbPosition, diceNum, ladderStart, ladderEnd int) {
	fmt.Printf("Player with Id:%s moved from position:%d to position:%d after rolling dice with num:%d and then to position:%d. Climbed by ladder, ladderStart:%d ladderEnd:%d\n", player.getID(), currPosition, ladderClimbPosition, diceNum, ladderEnd, ladderStart, ladderEnd)
}

func (b *board) printBoard() {
	line := "-------------------------------------------------------------"
	fmt.Println(line)
	for i := b.dimension - 1; i >= 0; i-- {
		fmt.Print("|")
		for j := b.dimension - 1; j >= 0; j-- {
			fmt.Print(b.square[i][j].getRepresentation(i, j, b.dimension))
		}
		fmt.Println()
	}
	fmt.Println(line)
	fmt.Println()
}

func IntArrayToString(a []int) string {
	b := make([]string, len(a))
	for i, v := range a {
		b[i] = strconv.Itoa(v)
	}

	return strings.Join(b, ",")
}

type game struct {
	board         *board
	players       []iPlayer
	numMoves      int
	gameStatus    GameStatus
	winningPlayer iPlayer
	numDice       int
}

func initGame(b *board, players []iPlayer, numDice int) *game {
	game := &game{
		board:      b,
		players:    players,
		gameStatus: GameInProgress,
		numDice:    numDice,
		numMoves:   0,
	}
	return game
}

func (g *game) play() error {
	fmt.Println("Initial Board")
	g.board.printBoard()
	for {
		for i := 0; i < len(g.players); i++ {
			currPlayer := g.players[i]
			diceNum := currPlayer.rollDice(g.numDice)
			playerWon := g.board.makeMove(currPlayer, diceNum)
			if playerWon {
				g.winningPlayer = currPlayer
				g.gameStatus = GameFinished
				break
			}
			g.board.printBoard()
		}

		g.numMoves = g.numMoves + 1
		if g.gameStatus != GameInProgress {
			break
		}
	}
	return nil
}

func (g *game) printResult() {
	switch g.gameStatus {
	case GameInProgress:
		fmt.Println("Game in Progress")
	case GameFinished:
		fmt.Println("Game Finished")
		fmt.Printf("Players with ID:%s won\n", g.winningPlayer.getID())
	default:
		fmt.Println("Invalid Game Status")
	}
	g.board.printBoard()
}

func main() {

	//Create 4 ladder

	snakes, err := createSnakes()
	if err != nil {
		fmt.Println(err)
	}

	ladders, err := createLadders()
	if err != nil {
		fmt.Println(err)
	}

	player1 := &humanPlayer{
		position: 1,
		id:       "a",
	}

	player2 := &computerPlayer{
		position: 1,
		id:       "b",
	}

	board := createBoard(snakes, ladders, 10, []iPlayer{player1, player2})

	game := initGame(board, []iPlayer{player1, player2}, 1)

	game.play()
	game.printResult()

}

func createSnakes() ([]snake, error) {
	// Create 4 snakes
	s1, err := createSnake(35, 25)
	if err != nil {
		fmt.Println(err)
		return nil, err
	}
	s2, err := createSnake(56, 36)
	if err != nil {
		fmt.Println(err)
		return nil, err
	}
	s3, err := createSnake(77, 57)
	if err != nil {
		fmt.Println(err)
		return nil, err
	}
	s4, err := createSnake(98, 78)
	if err != nil {
		fmt.Println(err)
		return nil, err
	}
	snakes := []snake{s1, s2, s3, s4}
	return snakes, nil
}

func createLadders() ([]ladder, error) {
	// Create 4 snakes
	s1, err := createLadder(26, 36)
	if err != nil {
		fmt.Println(err)
		return nil, err
	}
	s2, err := createLadder(37, 57)
	if err != nil {
		fmt.Println(err)
		return nil, err
	}
	s3, err := createLadder(58, 78)
	if err != nil {
		fmt.Println(err)
		return nil, err
	}
	s4, err := createLadder(74, 94)
	if err != nil {
		fmt.Println(err)
		return nil, err
	}
	ladders := []ladder{s1, s2, s3, s4}
	return ladders, nil
}

让我们运行这个程序

请注意,每次运行此程序时,输出都会有所不同

输出

Initial Board
-------------------------------------------------------------
|100 .|99  .|98  .|97  .|96  .|95  .|94  .|93  .|92  .|91  .|
|90  .|89  .|88  .|87  .|86  .|85  .|84  .|83  .|82  .|81  .|
|80  .|79  .|78  .|77  .|76  .|75  .|74  .|73  .|72  .|71  .|
|70  .|69  .|68  .|67  .|66  .|65  .|64  .|63  .|62  .|61  .|
|60  .|59  .|58  .|57  .|56  .|55  .|54  .|53  .|52  .|51  .|
|50  .|49  .|48  .|47  .|46  .|45  .|44  .|43  .|42  .|41  .|
|40  .|39  .|38  .|37  .|36  .|35  .|34  .|33  .|32  .|31  .|
|30  .|29  .|28  .|27  .|26  .|25  .|24  .|23  .|22  .|21  .|
|20  .|19  .|18  .|17  .|16  .|15  .|14  .|13  .|12  .|11  .|
|10  .|9   .|8   .|7   .|6   .|5   .|4   .|3   .|2   .|1   a,b|
-------------------------------------------------------------

Player with Id:a moved from position:1 to position:2 after rolling dice with num:1
-------------------------------------------------------------
|100 .|99  .|98  .|97  .|96  .|95  .|94  .|93  .|92  .|91  .|
|90  .|89  .|88  .|87  .|86  .|85  .|84  .|83  .|82  .|81  .|
|80  .|79  .|78  .|77  .|76  .|75  .|74  .|73  .|72  .|71  .|
|70  .|69  .|68  .|67  .|66  .|65  .|64  .|63  .|62  .|61  .|
|60  .|59  .|58  .|57  .|56  .|55  .|54  .|53  .|52  .|51  .|
|50  .|49  .|48  .|47  .|46  .|45  .|44  .|43  .|42  .|41  .|
|40  .|39  .|38  .|37  .|36  .|35  .|34  .|33  .|32  .|31  .|
|30  .|29  .|28  .|27  .|26  .|25  .|24  .|23  .|22  .|21  .|
|20  .|19  .|18  .|17  .|16  .|15  .|14  .|13  .|12  .|11  .|
|10  .|9   .|8   .|7   .|6   .|5   .|4   .|3   .|2   a|1   b|
-------------------------------------------------------------

Player with Id:b moved from position:1 to position:6 after rolling dice with num:5
-------------------------------------------------------------
|100 .|99  .|98  .|97  .|96  .|95  .|94  .|93  .|92  .|91  .|
|90  .|89  .|88  .|87  .|86  .|85  .|84  .|83  .|82  .|81  .|
|80  .|79  .|78  .|77  .|76  .|75  .|74  .|73  .|72  .|71  .|
|70  .|69  .|68  .|67  .|66  .|65  .|64  .|63  .|62  .|61  .|
|60  .|59  .|58  .|57  .|56  .|55  .|54  .|53  .|52  .|51  .|
|50  .|49  .|48  .|47  .|46  .|45  .|44  .|43  .|42  .|41  .|
|40  .|39  .|38  .|37  .|36  .|35  .|34  .|33  .|32  .|31  .|
|30  .|29  .|28  .|27  .|26  .|25  .|24  .|23  .|22  .|21  .|
|20  .|19  .|18  .|17  .|16  .|15  .|14  .|13  .|12  .|11  .|
|10  .|9   .|8   .|7   .|6   b|5   .|4   .|3   .|2   a|1   .|
-------------------------------------------------------------

Player with Id:a moved from position:2 to position:5 after rolling dice with num:3
-------------------------------------------------------------
|100 .|99  .|98  .|97  .|96  .|95  .|94  .|93  .|92  .|91  .|
|90  .|89  .|88  .|87  .|86  .|85  .|84  .|83  .|82  .|81  .|
|80  .|79  .|78  .|77  .|76  .|75  .|74  .|73  .|72  .|71  .|
|70  .|69  .|68  .|67  .|66  .|65  .|64  .|63  .|62  .|61  .|
|60  .|59  .|58  .|57  .|56  .|55  .|54  .|53  .|52  .|51  .|
|50  .|49  .|48  .|47  .|46  .|45  .|44  .|43  .|42  .|41  .|
|40  .|39  .|38  .|37  .|36  .|35  .|34  .|33  .|32  .|31  .|
|30  .|29  .|28  .|27  .|26  .|25  .|24  .|23  .|22  .|21  .|
|20  .|19  .|18  .|17  .|16  .|15  .|14  .|13  .|12  .|11  .|
|10  .|9   .|8   .|7   .|6   b|5   a|4   .|3   .|2   .|1   .|
-------------------------------------------------------------

Player with Id:b moved from position:6 to position:11 after rolling dice with num:5
-------------------------------------------------------------
|100 .|99  .|98  .|97  .|96  .|95  .|94  .|93  .|92  .|91  .|
|90  .|89  .|88  .|87  .|86  .|85  .|84  .|83  .|82  .|81  .|
|80  .|79  .|78  .|77  .|76  .|75  .|74  .|73  .|72  .|71  .|
|70  .|69  .|68  .|67  .|66  .|65  .|64  .|63  .|62  .|61  .|
|60  .|59  .|58  .|57  .|56  .|55  .|54  .|53  .|52  .|51  .|
|50  .|49  .|48  .|47  .|46  .|45  .|44  .|43  .|42  .|41  .|
|40  .|39  .|38  .|37  .|36  .|35  .|34  .|33  .|32  .|31  .|
|30  .|29  .|28  .|27  .|26  .|25  .|24  .|23  .|22  .|21  .|
|20  .|19  .|18  .|17  .|16  .|15  .|14  .|13  .|12  .|11  b|
|10  .|9   .|8   .|7   .|6   .|5   a|4   .|3   .|2   .|1   .|
-------------------------------------------------------------

Player with Id:a moved from position:5 to position:10 after rolling dice with num:5
-------------------------------------------------------------
|100 .|99  .|98  .|97  .|96  .|95  .|94  .|93  .|92  .|91  .|
|90  .|89  .|88  .|87  .|86  .|85  .|84  .|83  .|82  .|81  .|
|80  .|79  .|78  .|77  .|76  .|75  .|74  .|73  .|72  .|71  .|
|70  .|69  .|68  .|67  .|66  .|65  .|64  .|63  .|62  .|61  .|
|60  .|59  .|58  .|57  .|56  .|55  .|54  .|53  .|52  .|51  .|
|50  .|49  .|48  .|47  .|46  .|45  .|44  .|43  .|42  .|41  .|
|40  .|39  .|38  .|37  .|36  .|35  .|34  .|33  .|32  .|31  .|
|30  .|29  .|28  .|27  .|26  .|25  .|24  .|23  .|22  .|21  .|
|20  .|19  .|18  .|17  .|16  .|15  .|14  .|13  .|12  .|11  b|
|10  a|9   .|8   .|7   .|6   .|5   .|4   .|3   .|2   .|1   .|
-------------------------------------------------------------

Player with Id:b moved from position:11 to position:15 after rolling dice with num:4
-------------------------------------------------------------
|100 .|99  .|98  .|97  .|96  .|95  .|94  .|93  .|92  .|91  .|
|90  .|89  .|88  .|87  .|86  .|85  .|84  .|83  .|82  .|81  .|
|80  .|79  .|78  .|77  .|76  .|75  .|74  .|73  .|72  .|71  .|
|70  .|69  .|68  .|67  .|66  .|65  .|64  .|63  .|62  .|61  .|
|60  .|59  .|58  .|57  .|56  .|55  .|54  .|53  .|52  .|51  .|
|50  .|49  .|48  .|47  .|46  .|45  .|44  .|43  .|42  .|41  .|
|40  .|39  .|38  .|37  .|36  .|35  .|34  .|33  .|32  .|31  .|
|30  .|29  .|28  .|27  .|26  .|25  .|24  .|23  .|22  .|21  .|
|20  .|19  .|18  .|17  .|16  .|15  b|14  .|13  .|12  .|11  .|
|10  a|9   .|8   .|7   .|6   .|5   .|4   .|3   .|2   .|1   .|
-------------------------------------------------------------

Player with Id:a moved from position:10 to position:14 after rolling dice with num:4
-------------------------------------------------------------
|100 .|99  .|98  .|97  .|96  .|95  .|94  .|93  .|92  .|91  .|
|90  .|89  .|88  .|87  .|86  .|85  .|84  .|83  .|82  .|81  .|
|80  .|79  .|78  .|77  .|76  .|75  .|74  .|73  .|72  .|71  .|
|70  .|69  .|68  .|67  .|66  .|65  .|64  .|63  .|62  .|61  .|
|60  .|59  .|58  .|57  .|56  .|55  .|54  .|53  .|52  .|51  .|
|50  .|49  .|48  .|47  .|46  .|45  .|44  .|43  .|42  .|41  .|
|40  .|39  .|38  .|37  .|36  .|35  .|34  .|33  .|32  .|31  .|
|30  .|29  .|28  .|27  .|26  .|25  .|24  .|23  .|22  .|21  .|
|20  .|19  .|18  .|17  .|16  .|15  b|14  a|13  .|12  .|11  .|
|10  .|9   .|8   .|7   .|6   .|5   .|4   .|3   .|2   .|1   .|
-------------------------------------------------------------

Player with Id:b moved from position:15 to position:21 after rolling dice with num:6
-------------------------------------------------------------
|100 .|99  .|98  .|97  .|96  .|95  .|94  .|93  .|92  .|91  .|
|90  .|89  .|88  .|87  .|86  .|85  .|84  .|83  .|82  .|81  .|
|80  .|79  .|78  .|77  .|76  .|75  .|74  .|73  .|72  .|71  .|
|70  .|69  .|68  .|67  .|66  .|65  .|64  .|63  .|62  .|61  .|
|60  .|59  .|58  .|57  .|56  .|55  .|54  .|53  .|52  .|51  .|
|50  .|49  .|48  .|47  .|46  .|45  .|44  .|43  .|42  .|41  .|
|40  .|39  .|38  .|37  .|36  .|35  .|34  .|33  .|32  .|31  .|
|30  .|29  .|28  .|27  .|26  .|25  .|24  .|23  .|22  .|21  b|
|20  .|19  .|18  .|17  .|16  .|15  .|14  a|13  .|12  .|11  .|
|10  .|9   .|8   .|7   .|6   .|5   .|4   .|3   .|2   .|1   .|
-------------------------------------------------------------

Player with Id:a moved from position:14 to position:20 after rolling dice with num:6
-------------------------------------------------------------
|100 .|99  .|98  .|97  .|96  .|95  .|94  .|93  .|92  .|91  .|
|90  .|89  .|88  .|87  .|86  .|85  .|84  .|83  .|82  .|81  .|
|80  .|79  .|78  .|77  .|76  .|75  .|74  .|73  .|72  .|71  .|
|70  .|69  .|68  .|67  .|66  .|65  .|64  .|63  .|62  .|61  .|
|60  .|59  .|58  .|57  .|56  .|55  .|54  .|53  .|52  .|51  .|
|50  .|49  .|48  .|47  .|46  .|45  .|44  .|43  .|42  .|41  .|
|40  .|39  .|38  .|37  .|36  .|35  .|34  .|33  .|32  .|31  .|
|30  .|29  .|28  .|27  .|26  .|25  .|24  .|23  .|22  .|21  b|
|20  a|19  .|18  .|17  .|16  .|15  .|14  .|13  .|12  .|11  .|
|10  .|9   .|8   .|7   .|6   .|5   .|4   .|3   .|2   .|1   .|
-------------------------------------------------------------

Player with Id:b moved from position:21 to position:24 after rolling dice with num:3
-------------------------------------------------------------
|100 .|99  .|98  .|97  .|96  .|95  .|94  .|93  .|92  .|91  .|
|90  .|89  .|88  .|87  .|86  .|85  .|84  .|83  .|82  .|81  .|
|80  .|79  .|78  .|77  .|76  .|75  .|74  .|73  .|72  .|71  .|
|70  .|69  .|68  .|67  .|66  .|65  .|64  .|63  .|62  .|61  .|
|60  .|59  .|58  .|57  .|56  .|55  .|54  .|53  .|52  .|51  .|
|50  .|49  .|48  .|47  .|46  .|45  .|44  .|43  .|42  .|41  .|
|40  .|39  .|38  .|37  .|36  .|35  .|34  .|33  .|32  .|31  .|
|30  .|29  .|28  .|27  .|26  .|25  .|24  b|23  .|22  .|21  .|
|20  a|19  .|18  .|17  .|16  .|15  .|14  .|13  .|12  .|11  .|
|10  .|9   .|8   .|7   .|6   .|5   .|4   .|3   .|2   .|1   .|
-------------------------------------------------------------

Player with Id:a moved from position:20 to position:25 after rolling dice with num:5
-------------------------------------------------------------
|100 .|99  .|98  .|97  .|96  .|95  .|94  .|93  .|92  .|91  .|
|90  .|89  .|88  .|87  .|86  .|85  .|84  .|83  .|82  .|81  .|
|80  .|79  .|78  .|77  .|76  .|75  .|74  .|73  .|72  .|71  .|
|70  .|69  .|68  .|67  .|66  .|65  .|64  .|63  .|62  .|61  .|
|60  .|59  .|58  .|57  .|56  .|55  .|54  .|53  .|52  .|51  .|
|50  .|49  .|48  .|47  .|46  .|45  .|44  .|43  .|42  .|41  .|
|40  .|39  .|38  .|37  .|36  .|35  .|34  .|33  .|32  .|31  .|
|30  .|29  .|28  .|27  .|26  .|25  a|24  b|23  .|22  .|21  .|
|20  .|19  .|18  .|17  .|16  .|15  .|14  .|13  .|12  .|11  .|
|10  .|9   .|8   .|7   .|6   .|5   .|4   .|3   .|2   .|1   .|
-------------------------------------------------------------

Player with Id:b moved from position:24 to position:26 after rolling dice with num:2 and then to position:36\. Climbed by ladder, ladderStart:26 ladderEnd:36
-------------------------------------------------------------
|100 .|99  .|98  .|97  .|96  .|95  .|94  .|93  .|92  .|91  .|
|90  .|89  .|88  .|87  .|86  .|85  .|84  .|83  .|82  .|81  .|
|80  .|79  .|78  .|77  .|76  .|75  .|74  .|73  .|72  .|71  .|
|70  .|69  .|68  .|67  .|66  .|65  .|64  .|63  .|62  .|61  .|
|60  .|59  .|58  .|57  .|56  .|55  .|54  .|53  .|52  .|51  .|
|50  .|49  .|48  .|47  .|46  .|45  .|44  .|43  .|42  .|41  .|
|40  .|39  .|38  .|37  .|36  b|35  .|34  .|33  .|32  .|31  .|
|30  .|29  .|28  .|27  .|26  .|25  a|24  .|23  .|22  .|21  .|
|20  .|19  .|18  .|17  .|16  .|15  .|14  .|13  .|12  .|11  .|
|10  .|9   .|8   .|7   .|6   .|5   .|4   .|3   .|2   .|1   .|
-------------------------------------------------------------

Player with Id:a moved from position:25 to position:27 after rolling dice with num:2
-------------------------------------------------------------
|100 .|99  .|98  .|97  .|96  .|95  .|94  .|93  .|92  .|91  .|
|90  .|89  .|88  .|87  .|86  .|85  .|84  .|83  .|82  .|81  .|
|80  .|79  .|78  .|77  .|76  .|75  .|74  .|73  .|72  .|71  .|
|70  .|69  .|68  .|67  .|66  .|65  .|64  .|63  .|62  .|61  .|
|60  .|59  .|58  .|57  .|56  .|55  .|54  .|53  .|52  .|51  .|
|50  .|49  .|48  .|47  .|46  .|45  .|44  .|43  .|42  .|41  .|
|40  .|39  .|38  .|37  .|36  b|35  .|34  .|33  .|32  .|31  .|
|30  .|29  .|28  .|27  a|26  .|25  .|24  .|23  .|22  .|21  .|
|20  .|19  .|18  .|17  .|16  .|15  .|14  .|13  .|12  .|11  .|
|10  .|9   .|8   .|7   .|6   .|5   .|4   .|3   .|2   .|1   .|
-------------------------------------------------------------

Player with Id:b moved from position:36 to position:37 after rolling dice with num:1 and then to position:57\. Climbed by ladder, ladderStart:37 ladderEnd:57
-------------------------------------------------------------
|100 .|99  .|98  .|97  .|96  .|95  .|94  .|93  .|92  .|91  .|
|90  .|89  .|88  .|87  .|86  .|85  .|84  .|83  .|82  .|81  .|
|80  .|79  .|78  .|77  .|76  .|75  .|74  .|73  .|72  .|71  .|
|70  .|69  .|68  .|67  .|66  .|65  .|64  .|63  .|62  .|61  .|
|60  .|59  .|58  .|57  b|56  .|55  .|54  .|53  .|52  .|51  .|
|50  .|49  .|48  .|47  .|46  .|45  .|44  .|43  .|42  .|41  .|
|40  .|39  .|38  .|37  .|36  .|35  .|34  .|33  .|32  .|31  .|
|30  .|29  .|28  .|27  a|26  .|25  .|24  .|23  .|22  .|21  .|
|20  .|19  .|18  .|17  .|16  .|15  .|14  .|13  .|12  .|11  .|
|10  .|9   .|8   .|7   .|6   .|5   .|4   .|3   .|2   .|1   .|
-------------------------------------------------------------

Player with Id:a moved from position:27 to position:28 after rolling dice with num:1
-------------------------------------------------------------
|100 .|99  .|98  .|97  .|96  .|95  .|94  .|93  .|92  .|91  .|
|90  .|89  .|88  .|87  .|86  .|85  .|84  .|83  .|82  .|81  .|
|80  .|79  .|78  .|77  .|76  .|75  .|74  .|73  .|72  .|71  .|
|70  .|69  .|68  .|67  .|66  .|65  .|64  .|63  .|62  .|61  .|
|60  .|59  .|58  .|57  b|56  .|55  .|54  .|53  .|52  .|51  .|
|50  .|49  .|48  .|47  .|46  .|45  .|44  .|43  .|42  .|41  .|
|40  .|39  .|38  .|37  .|36  .|35  .|34  .|33  .|32  .|31  .|
|30  .|29  .|28  a|27  .|26  .|25  .|24  .|23  .|22  .|21  .|
|20  .|19  .|18  .|17  .|16  .|15  .|14  .|13  .|12  .|11  .|
|10  .|9   .|8   .|7   .|6   .|5   .|4   .|3   .|2   .|1   .|
-------------------------------------------------------------

Player with Id:b moved from position:57 to position:62 after rolling dice with num:5
-------------------------------------------------------------
|100 .|99  .|98  .|97  .|96  .|95  .|94  .|93  .|92  .|91  .|
|90  .|89  .|88  .|87  .|86  .|85  .|84  .|83  .|82  .|81  .|
|80  .|79  .|78  .|77  .|76  .|75  .|74  .|73  .|72  .|71  .|
|70  .|69  .|68  .|67  .|66  .|65  .|64  .|63  .|62  b|61  .|
|60  .|59  .|58  .|57  .|56  .|55  .|54  .|53  .|52  .|51  .|
|50  .|49  .|48  .|47  .|46  .|45  .|44  .|43  .|42  .|41  .|
|40  .|39  .|38  .|37  .|36  .|35  .|34  .|33  .|32  .|31  .|
|30  .|29  .|28  a|27  .|26  .|25  .|24  .|23  .|22  .|21  .|
|20  .|19  .|18  .|17  .|16  .|15  .|14  .|13  .|12  .|11  .|
|10  .|9   .|8   .|7   .|6   .|5   .|4   .|3   .|2   .|1   .|
-------------------------------------------------------------

Player with Id:a moved from position:28 to position:31 after rolling dice with num:3
-------------------------------------------------------------
|100 .|99  .|98  .|97  .|96  .|95  .|94  .|93  .|92  .|91  .|
|90  .|89  .|88  .|87  .|86  .|85  .|84  .|83  .|82  .|81  .|
|80  .|79  .|78  .|77  .|76  .|75  .|74  .|73  .|72  .|71  .|
|70  .|69  .|68  .|67  .|66  .|65  .|64  .|63  .|62  b|61  .|
|60  .|59  .|58  .|57  .|56  .|55  .|54  .|53  .|52  .|51  .|
|50  .|49  .|48  .|47  .|46  .|45  .|44  .|43  .|42  .|41  .|
|40  .|39  .|38  .|37  .|36  .|35  .|34  .|33  .|32  .|31  a|
|30  .|29  .|28  .|27  .|26  .|25  .|24  .|23  .|22  .|21  .|
|20  .|19  .|18  .|17  .|16  .|15  .|14  .|13  .|12  .|11  .|
|10  .|9   .|8   .|7   .|6   .|5   .|4   .|3   .|2   .|1   .|
-------------------------------------------------------------

Player with Id:b moved from position:62 to position:63 after rolling dice with num:1
-------------------------------------------------------------
|100 .|99  .|98  .|97  .|96  .|95  .|94  .|93  .|92  .|91  .|
|90  .|89  .|88  .|87  .|86  .|85  .|84  .|83  .|82  .|81  .|
|80  .|79  .|78  .|77  .|76  .|75  .|74  .|73  .|72  .|71  .|
|70  .|69  .|68  .|67  .|66  .|65  .|64  .|63  b|62  .|61  .|
|60  .|59  .|58  .|57  .|56  .|55  .|54  .|53  .|52  .|51  .|
|50  .|49  .|48  .|47  .|46  .|45  .|44  .|43  .|42  .|41  .|
|40  .|39  .|38  .|37  .|36  .|35  .|34  .|33  .|32  .|31  a|
|30  .|29  .|28  .|27  .|26  .|25  .|24  .|23  .|22  .|21  .|
|20  .|19  .|18  .|17  .|16  .|15  .|14  .|13  .|12  .|11  .|
|10  .|9   .|8   .|7   .|6   .|5   .|4   .|3   .|2   .|1   .|
-------------------------------------------------------------

Player with Id:a moved from position:31 to position:34 after rolling dice with num:3
-------------------------------------------------------------
|100 .|99  .|98  .|97  .|96  .|95  .|94  .|93  .|92  .|91  .|
|90  .|89  .|88  .|87  .|86  .|85  .|84  .|83  .|82  .|81  .|
|80  .|79  .|78  .|77  .|76  .|75  .|74  .|73  .|72  .|71  .|
|70  .|69  .|68  .|67  .|66  .|65  .|64  .|63  b|62  .|61  .|
|60  .|59  .|58  .|57  .|56  .|55  .|54  .|53  .|52  .|51  .|
|50  .|49  .|48  .|47  .|46  .|45  .|44  .|43  .|42  .|41  .|
|40  .|39  .|38  .|37  .|36  .|35  .|34  a|33  .|32  .|31  .|
|30  .|29  .|28  .|27  .|26  .|25  .|24  .|23  .|22  .|21  .|
|20  .|19  .|18  .|17  .|16  .|15  .|14  .|13  .|12  .|11  .|
|10  .|9   .|8   .|7   .|6   .|5   .|4   .|3   .|2   .|1   .|
-------------------------------------------------------------

Player with Id:b moved from position:63 to position:68 after rolling dice with num:5
-------------------------------------------------------------
|100 .|99  .|98  .|97  .|96  .|95  .|94  .|93  .|92  .|91  .|
|90  .|89  .|88  .|87  .|86  .|85  .|84  .|83  .|82  .|81  .|
|80  .|79  .|78  .|77  .|76  .|75  .|74  .|73  .|72  .|71  .|
|70  .|69  .|68  b|67  .|66  .|65  .|64  .|63  .|62  .|61  .|
|60  .|59  .|58  .|57  .|56  .|55  .|54  .|53  .|52  .|51  .|
|50  .|49  .|48  .|47  .|46  .|45  .|44  .|43  .|42  .|41  .|
|40  .|39  .|38  .|37  .|36  .|35  .|34  a|33  .|32  .|31  .|
|30  .|29  .|28  .|27  .|26  .|25  .|24  .|23  .|22  .|21  .|
|20  .|19  .|18  .|17  .|16  .|15  .|14  .|13  .|12  .|11  .|
|10  .|9   .|8   .|7   .|6   .|5   .|4   .|3   .|2   .|1   .|
-------------------------------------------------------------

Player with Id:a moved from position:34 to position:35 after rolling dice with num:1 and then to position:25\. Bit by snake, snakeStart:35 snakeEnd:25
-------------------------------------------------------------
|100 .|99  .|98  .|97  .|96  .|95  .|94  .|93  .|92  .|91  .|
|90  .|89  .|88  .|87  .|86  .|85  .|84  .|83  .|82  .|81  .|
|80  .|79  .|78  .|77  .|76  .|75  .|74  .|73  .|72  .|71  .|
|70  .|69  .|68  b|67  .|66  .|65  .|64  .|63  .|62  .|61  .|
|60  .|59  .|58  .|57  .|56  .|55  .|54  .|53  .|52  .|51  .|
|50  .|49  .|48  .|47  .|46  .|45  .|44  .|43  .|42  .|41  .|
|40  .|39  .|38  .|37  .|36  .|35  .|34  .|33  .|32  .|31  .|
|30  .|29  .|28  .|27  .|26  .|25  a|24  .|23  .|22  .|21  .|
|20  .|19  .|18  .|17  .|16  .|15  .|14  .|13  .|12  .|11  .|
|10  .|9   .|8   .|7   .|6   .|5   .|4   .|3   .|2   .|1   .|
-------------------------------------------------------------

Player with Id:b moved from position:68 to position:72 after rolling dice with num:4
-------------------------------------------------------------
|100 .|99  .|98  .|97  .|96  .|95  .|94  .|93  .|92  .|91  .|
|90  .|89  .|88  .|87  .|86  .|85  .|84  .|83  .|82  .|81  .|
|80  .|79  .|78  .|77  .|76  .|75  .|74  .|73  .|72  b|71  .|
|70  .|69  .|68  .|67  .|66  .|65  .|64  .|63  .|62  .|61  .|
|60  .|59  .|58  .|57  .|56  .|55  .|54  .|53  .|52  .|51  .|
|50  .|49  .|48  .|47  .|46  .|45  .|44  .|43  .|42  .|41  .|
|40  .|39  .|38  .|37  .|36  .|35  .|34  .|33  .|32  .|31  .|
|30  .|29  .|28  .|27  .|26  .|25  a|24  .|23  .|22  .|21  .|
|20  .|19  .|18  .|17  .|16  .|15  .|14  .|13  .|12  .|11  .|
|10  .|9   .|8   .|7   .|6   .|5   .|4   .|3   .|2   .|1   .|
-------------------------------------------------------------

Player with Id:a moved from position:25 to position:30 after rolling dice with num:5
-------------------------------------------------------------
|100 .|99  .|98  .|97  .|96  .|95  .|94  .|93  .|92  .|91  .|
|90  .|89  .|88  .|87  .|86  .|85  .|84  .|83  .|82  .|81  .|
|80  .|79  .|78  .|77  .|76  .|75  .|74  .|73  .|72  b|71  .|
|70  .|69  .|68  .|67  .|66  .|65  .|64  .|63  .|62  .|61  .|
|60  .|59  .|58  .|57  .|56  .|55  .|54  .|53  .|52  .|51  .|
|50  .|49  .|48  .|47  .|46  .|45  .|44  .|43  .|42  .|41  .|
|40  .|39  .|38  .|37  .|36  .|35  .|34  .|33  .|32  .|31  .|
|30  a|29  .|28  .|27  .|26  .|25  .|24  .|23  .|22  .|21  .|
|20  .|19  .|18  .|17  .|16  .|15  .|14  .|13  .|12  .|11  .|
|10  .|9   .|8   .|7   .|6   .|5   .|4   .|3   .|2   .|1   .|
-------------------------------------------------------------

Player with Id:b moved from position:72 to position:75 after rolling dice with num:3
-------------------------------------------------------------
|100 .|99  .|98  .|97  .|96  .|95  .|94  .|93  .|92  .|91  .|
|90  .|89  .|88  .|87  .|86  .|85  .|84  .|83  .|82  .|81  .|
|80  .|79  .|78  .|77  .|76  .|75  b|74  .|73  .|72  .|71  .|
|70  .|69  .|68  .|67  .|66  .|65  .|64  .|63  .|62  .|61  .|
|60  .|59  .|58  .|57  .|56  .|55  .|54  .|53  .|52  .|51  .|
|50  .|49  .|48  .|47  .|46  .|45  .|44  .|43  .|42  .|41  .|
|40  .|39  .|38  .|37  .|36  .|35  .|34  .|33  .|32  .|31  .|
|30  a|29  .|28  .|27  .|26  .|25  .|24  .|23  .|22  .|21  .|
|20  .|19  .|18  .|17  .|16  .|15  .|14  .|13  .|12  .|11  .|
|10  .|9   .|8   .|7   .|6   .|5   .|4   .|3   .|2   .|1   .|
-------------------------------------------------------------

Player with Id:a moved from position:30 to position:36 after rolling dice with num:6
-------------------------------------------------------------
|100 .|99  .|98  .|97  .|96  .|95  .|94  .|93  .|92  .|91  .|
|90  .|89  .|88  .|87  .|86  .|85  .|84  .|83  .|82  .|81  .|
|80  .|79  .|78  .|77  .|76  .|75  b|74  .|73  .|72  .|71  .|
|70  .|69  .|68  .|67  .|66  .|65  .|64  .|63  .|62  .|61  .|
|60  .|59  .|58  .|57  .|56  .|55  .|54  .|53  .|52  .|51  .|
|50  .|49  .|48  .|47  .|46  .|45  .|44  .|43  .|42  .|41  .|
|40  .|39  .|38  .|37  .|36  a|35  .|34  .|33  .|32  .|31  .|
|30  .|29  .|28  .|27  .|26  .|25  .|24  .|23  .|22  .|21  .|
|20  .|19  .|18  .|17  .|16  .|15  .|14  .|13  .|12  .|11  .|
|10  .|9   .|8   .|7   .|6   .|5   .|4   .|3   .|2   .|1   .|
-------------------------------------------------------------

Player with Id:b moved from position:75 to position:76 after rolling dice with num:1
-------------------------------------------------------------
|100 .|99  .|98  .|97  .|96  .|95  .|94  .|93  .|92  .|91  .|
|90  .|89  .|88  .|87  .|86  .|85  .|84  .|83  .|82  .|81  .|
|80  .|79  .|78  .|77  .|76  b|75  .|74  .|73  .|72  .|71  .|
|70  .|69  .|68  .|67  .|66  .|65  .|64  .|63  .|62  .|61  .|
|60  .|59  .|58  .|57  .|56  .|55  .|54  .|53  .|52  .|51  .|
|50  .|49  .|48  .|47  .|46  .|45  .|44  .|43  .|42  .|41  .|
|40  .|39  .|38  .|37  .|36  a|35  .|34  .|33  .|32  .|31  .|
|30  .|29  .|28  .|27  .|26  .|25  .|24  .|23  .|22  .|21  .|
|20  .|19  .|18  .|17  .|16  .|15  .|14  .|13  .|12  .|11  .|
|10  .|9   .|8   .|7   .|6   .|5   .|4   .|3   .|2   .|1   .|
-------------------------------------------------------------

Player with Id:a moved from position:36 to position:37 after rolling dice with num:1 and then to position:57\. Climbed by ladder, ladderStart:37 ladderEnd:57
-------------------------------------------------------------
|100 .|99  .|98  .|97  .|96  .|95  .|94  .|93  .|92  .|91  .|
|90  .|89  .|88  .|87  .|86  .|85  .|84  .|83  .|82  .|81  .|
|80  .|79  .|78  .|77  .|76  b|75  .|74  .|73  .|72  .|71  .|
|70  .|69  .|68  .|67  .|66  .|65  .|64  .|63  .|62  .|61  .|
|60  .|59  .|58  .|57  a|56  .|55  .|54  .|53  .|52  .|51  .|
|50  .|49  .|48  .|47  .|46  .|45  .|44  .|43  .|42  .|41  .|
|40  .|39  .|38  .|37  .|36  .|35  .|34  .|33  .|32  .|31  .|
|30  .|29  .|28  .|27  .|26  .|25  .|24  .|23  .|22  .|21  .|
|20  .|19  .|18  .|17  .|16  .|15  .|14  .|13  .|12  .|11  .|
|10  .|9   .|8   .|7   .|6   .|5   .|4   .|3   .|2   .|1   .|
-------------------------------------------------------------

Player with Id:b moved from position:76 to position:80 after rolling dice with num:4
-------------------------------------------------------------
|100 .|99  .|98  .|97  .|96  .|95  .|94  .|93  .|92  .|91  .|
|90  .|89  .|88  .|87  .|86  .|85  .|84  .|83  .|82  .|81  .|
|80  b|79  .|78  .|77  .|76  .|75  .|74  .|73  .|72  .|71  .|
|70  .|69  .|68  .|67  .|66  .|65  .|64  .|63  .|62  .|61  .|
|60  .|59  .|58  .|57  a|56  .|55  .|54  .|53  .|52  .|51  .|
|50  .|49  .|48  .|47  .|46  .|45  .|44  .|43  .|42  .|41  .|
|40  .|39  .|38  .|37  .|36  .|35  .|34  .|33  .|32  .|31  .|
|30  .|29  .|28  .|27  .|26  .|25  .|24  .|23  .|22  .|21  .|
|20  .|19  .|18  .|17  .|16  .|15  .|14  .|13  .|12  .|11  .|
|10  .|9   .|8   .|7   .|6   .|5   .|4   .|3   .|2   .|1   .|
-------------------------------------------------------------

Player with Id:a moved from position:57 to position:59 after rolling dice with num:2
-------------------------------------------------------------
|100 .|99  .|98  .|97  .|96  .|95  .|94  .|93  .|92  .|91  .|
|90  .|89  .|88  .|87  .|86  .|85  .|84  .|83  .|82  .|81  .|
|80  b|79  .|78  .|77  .|76  .|75  .|74  .|73  .|72  .|71  .|
|70  .|69  .|68  .|67  .|66  .|65  .|64  .|63  .|62  .|61  .|
|60  .|59  a|58  .|57  .|56  .|55  .|54  .|53  .|52  .|51  .|
|50  .|49  .|48  .|47  .|46  .|45  .|44  .|43  .|42  .|41  .|
|40  .|39  .|38  .|37  .|36  .|35  .|34  .|33  .|32  .|31  .|
|30  .|29  .|28  .|27  .|26  .|25  .|24  .|23  .|22  .|21  .|
|20  .|19  .|18  .|17  .|16  .|15  .|14  .|13  .|12  .|11  .|
|10  .|9   .|8   .|7   .|6   .|5   .|4   .|3   .|2   .|1   .|
-------------------------------------------------------------

Player with Id:b moved from position:80 to position:86 after rolling dice with num:6
-------------------------------------------------------------
|100 .|99  .|98  .|97  .|96  .|95  .|94  .|93  .|92  .|91  .|
|90  .|89  .|88  .|87  .|86  b|85  .|84  .|83  .|82  .|81  .|
|80  .|79  .|78  .|77  .|76  .|75  .|74  .|73  .|72  .|71  .|
|70  .|69  .|68  .|67  .|66  .|65  .|64  .|63  .|62  .|61  .|
|60  .|59  a|58  .|57  .|56  .|55  .|54  .|53  .|52  .|51  .|
|50  .|49  .|48  .|47  .|46  .|45  .|44  .|43  .|42  .|41  .|
|40  .|39  .|38  .|37  .|36  .|35  .|34  .|33  .|32  .|31  .|
|30  .|29  .|28  .|27  .|26  .|25  .|24  .|23  .|22  .|21  .|
|20  .|19  .|18  .|17  .|16  .|15  .|14  .|13  .|12  .|11  .|
|10  .|9   .|8   .|7   .|6   .|5   .|4   .|3   .|2   .|1   .|
-------------------------------------------------------------

Player with Id:a moved from position:59 to position:62 after rolling dice with num:3
-------------------------------------------------------------
|100 .|99  .|98  .|97  .|96  .|95  .|94  .|93  .|92  .|91  .|
|90  .|89  .|88  .|87  .|86  b|85  .|84  .|83  .|82  .|81  .|
|80  .|79  .|78  .|77  .|76  .|75  .|74  .|73  .|72  .|71  .|
|70  .|69  .|68  .|67  .|66  .|65  .|64  .|63  .|62  a|61  .|
|60  .|59  .|58  .|57  .|56  .|55  .|54  .|53  .|52  .|51  .|
|50  .|49  .|48  .|47  .|46  .|45  .|44  .|43  .|42  .|41  .|
|40  .|39  .|38  .|37  .|36  .|35  .|34  .|33  .|32  .|31  .|
|30  .|29  .|28  .|27  .|26  .|25  .|24  .|23  .|22  .|21  .|
|20  .|19  .|18  .|17  .|16  .|15  .|14  .|13  .|12  .|11  .|
|10  .|9   .|8   .|7   .|6   .|5   .|4   .|3   .|2   .|1   .|
-------------------------------------------------------------

Player with Id:b moved from position:86 to position:92 after rolling dice with num:6
-------------------------------------------------------------
|100 .|99  .|98  .|97  .|96  .|95  .|94  .|93  .|92  b|91  .|
|90  .|89  .|88  .|87  .|86  .|85  .|84  .|83  .|82  .|81  .|
|80  .|79  .|78  .|77  .|76  .|75  .|74  .|73  .|72  .|71  .|
|70  .|69  .|68  .|67  .|66  .|65  .|64  .|63  .|62  a|61  .|
|60  .|59  .|58  .|57  .|56  .|55  .|54  .|53  .|52  .|51  .|
|50  .|49  .|48  .|47  .|46  .|45  .|44  .|43  .|42  .|41  .|
|40  .|39  .|38  .|37  .|36  .|35  .|34  .|33  .|32  .|31  .|
|30  .|29  .|28  .|27  .|26  .|25  .|24  .|23  .|22  .|21  .|
|20  .|19  .|18  .|17  .|16  .|15  .|14  .|13  .|12  .|11  .|
|10  .|9   .|8   .|7   .|6   .|5   .|4   .|3   .|2   .|1   .|
-------------------------------------------------------------

Player with Id:a moved from position:62 to position:63 after rolling dice with num:1
-------------------------------------------------------------
|100 .|99  .|98  .|97  .|96  .|95  .|94  .|93  .|92  b|91  .|
|90  .|89  .|88  .|87  .|86  .|85  .|84  .|83  .|82  .|81  .|
|80  .|79  .|78  .|77  .|76  .|75  .|74  .|73  .|72  .|71  .|
|70  .|69  .|68  .|67  .|66  .|65  .|64  .|63  a|62  .|61  .|
|60  .|59  .|58  .|57  .|56  .|55  .|54  .|53  .|52  .|51  .|
|50  .|49  .|48  .|47  .|46  .|45  .|44  .|43  .|42  .|41  .|
|40  .|39  .|38  .|37  .|36  .|35  .|34  .|33  .|32  .|31  .|
|30  .|29  .|28  .|27  .|26  .|25  .|24  .|23  .|22  .|21  .|
|20  .|19  .|18  .|17  .|16  .|15  .|14  .|13  .|12  .|11  .|
|10  .|9   .|8   .|7   .|6   .|5   .|4   .|3   .|2   .|1   .|
-------------------------------------------------------------

Player with Id:b moved from position:92 to position:95 after rolling dice with num:3
-------------------------------------------------------------
|100 .|99  .|98  .|97  .|96  .|95  b|94  .|93  .|92  .|91  .|
|90  .|89  .|88  .|87  .|86  .|85  .|84  .|83  .|82  .|81  .|
|80  .|79  .|78  .|77  .|76  .|75  .|74  .|73  .|72  .|71  .|
|70  .|69  .|68  .|67  .|66  .|65  .|64  .|63  a|62  .|61  .|
|60  .|59  .|58  .|57  .|56  .|55  .|54  .|53  .|52  .|51  .|
|50  .|49  .|48  .|47  .|46  .|45  .|44  .|43  .|42  .|41  .|
|40  .|39  .|38  .|37  .|36  .|35  .|34  .|33  .|32  .|31  .|
|30  .|29  .|28  .|27  .|26  .|25  .|24  .|23  .|22  .|21  .|
|20  .|19  .|18  .|17  .|16  .|15  .|14  .|13  .|12  .|11  .|
|10  .|9   .|8   .|7   .|6   .|5   .|4   .|3   .|2   .|1   .|
-------------------------------------------------------------

Player with Id:a moved from position:63 to position:69 after rolling dice with num:6
-------------------------------------------------------------
|100 .|99  .|98  .|97  .|96  .|95  b|94  .|93  .|92  .|91  .|
|90  .|89  .|88  .|87  .|86  .|85  .|84  .|83  .|82  .|81  .|
|80  .|79  .|78  .|77  .|76  .|75  .|74  .|73  .|72  .|71  .|
|70  .|69  a|68  .|67  .|66  .|65  .|64  .|63  .|62  .|61  .|
|60  .|59  .|58  .|57  .|56  .|55  .|54  .|53  .|52  .|51  .|
|50  .|49  .|48  .|47  .|46  .|45  .|44  .|43  .|42  .|41  .|
|40  .|39  .|38  .|37  .|36  .|35  .|34  .|33  .|32  .|31  .|
|30  .|29  .|28  .|27  .|26  .|25  .|24  .|23  .|22  .|21  .|
|20  .|19  .|18  .|17  .|16  .|15  .|14  .|13  .|12  .|11  .|
|10  .|9   .|8   .|7   .|6   .|5   .|4   .|3   .|2   .|1   .|
-------------------------------------------------------------

Player with Id:b moved from position:95 to position:95 after rolling dice with num:6
-------------------------------------------------------------
|100 .|99  .|98  .|97  .|96  .|95  b|94  .|93  .|92  .|91  .|
|90  .|89  .|88  .|87  .|86  .|85  .|84  .|83  .|82  .|81  .|
|80  .|79  .|78  .|77  .|76  .|75  .|74  .|73  .|72  .|71  .|
|70  .|69  a|68  .|67  .|66  .|65  .|64  .|63  .|62  .|61  .|
|60  .|59  .|58  .|57  .|56  .|55  .|54  .|53  .|52  .|51  .|
|50  .|49  .|48  .|47  .|46  .|45  .|44  .|43  .|42  .|41  .|
|40  .|39  .|38  .|37  .|36  .|35  .|34  .|33  .|32  .|31  .|
|30  .|29  .|28  .|27  .|26  .|25  .|24  .|23  .|22  .|21  .|
|20  .|19  .|18  .|17  .|16  .|15  .|14  .|13  .|12  .|11  .|
|10  .|9   .|8   .|7   .|6   .|5   .|4   .|3   .|2   .|1   .|
-------------------------------------------------------------

Player with Id:a moved from position:69 to position:70 after rolling dice with num:1
-------------------------------------------------------------
|100 .|99  .|98  .|97  .|96  .|95  b|94  .|93  .|92  .|91  .|
|90  .|89  .|88  .|87  .|86  .|85  .|84  .|83  .|82  .|81  .|
|80  .|79  .|78  .|77  .|76  .|75  .|74  .|73  .|72  .|71  .|
|70  a|69  .|68  .|67  .|66  .|65  .|64  .|63  .|62  .|61  .|
|60  .|59  .|58  .|57  .|56  .|55  .|54  .|53  .|52  .|51  .|
|50  .|49  .|48  .|47  .|46  .|45  .|44  .|43  .|42  .|41  .|
|40  .|39  .|38  .|37  .|36  .|35  .|34  .|33  .|32  .|31  .|
|30  .|29  .|28  .|27  .|26  .|25  .|24  .|23  .|22  .|21  .|
|20  .|19  .|18  .|17  .|16  .|15  .|14  .|13  .|12  .|11  .|
|10  .|9   .|8   .|7   .|6   .|5   .|4   .|3   .|2   .|1   .|
-------------------------------------------------------------

Player with Id:b moved from position:95 to position:100 after rolling dice with num:5
Game Finished
Players with ID:b won
-------------------------------------------------------------
|100 b|99  .|98  .|97  .|96  .|95  .|94  .|93  .|92  .|91  .|
|90  .|89  .|88  .|87  .|86  .|85  .|84  .|83  .|82  .|81  .|
|80  .|79  .|78  .|77  .|76  .|75  .|74  .|73  .|72  .|71  .|
|70  a|69  .|68  .|67  .|66  .|65  .|64  .|63  .|62  .|61  .|
|60  .|59  .|58  .|57  .|56  .|55  .|54  .|53  .|52  .|51  .|
|50  .|49  .|48  .|47  .|46  .|45  .|44  .|43  .|42  .|41  .|
|40  .|39  .|38  .|37  .|36  .|35  .|34  .|33  .|32  .|31  .|
|30  .|29  .|28  .|27  .|26  .|25  .|24  .|23  .|22  .|21  .|
|20  .|19  .|18  .|17  .|16  .|15  .|14  .|13  .|12  .|11  .|
|10  .|9   .|8   .|7   .|6   .|5   .|4   .|3   .|2   .|1   .|
-------------------------------------------------------------

结论

这篇文章主要讲解了“蛇与梯子”程序的低级设计。希望你喜欢这篇文章。请在评论中分享你的反馈。

注意: 查看我们的系统设计教程系列 System Design Questions

附近朋友系统设计

原文:techbyexample.com/nearby-friends-system-design/

目录

  • 概述

  • 功能需求

  • 非功能性需求

  • 数据存储

  • 位置更新需求

  • 高层设计

  • API 设计

    • 位置更新 API

    • 开启或关闭位置偏好设置

  • 位置共享的工作原理

  • 第一种方法 – 使用分布式 Redis

    • 用户首次上线

    • 当用户已经在线时

    • 当用户离线或关闭在线状态时

  • 第二种方法 – 使用 Redis 发布/订阅

    • 当用户首次上线时

    • 当用户已经在线时

    • 当用户离线或关闭在线状态时

  • 非功能性需求

    • 扩展性
  • Redis 发布/订阅服务器的扩展性

    • 频道数量

    • Redis 发布/订阅集群中使用的实例类型

    • 集群中的实例数量或集群的横向扩展性

    • 发布到频道的消息的平均大小

    • 集群的复制

  • 结论

概述

目标是设计附近朋友功能。当我们提到“附近”时,意味着朋友的位置在不断变化。位置变化时,他们的位置会不断更新,并与他们的附近朋友共享。

让我们首先列出系统的功能需求

功能需求

  • 用户在线时,应能查看所有朋友的当前位置信息,反之亦然。

  • 每当朋友的位置发生变化时,应当反映在用户的视图中,反之亦然。

  • 我们应该能够存储用户的位置信息历史,随着位置的变化进行更新。

  • 用户应能够控制是否希望自己显示为附近的朋友。换句话说,他们有能力关闭该功能。

非功能需求

  • 系统的延迟应尽可能小。

  • 显示给用户的朋友位置将是最终一致的。

关于位置共享更新,系统应最终一致。这意味着用户的位置将以最终一致的方式传达给他的朋友,虽然用户的位置可能会暂时不正确地显示给朋友,但最终会显示正确的位置。

在设计附近朋友功能时,第一个问题是朋友的位置信息如何更新到客户端应用中。为此,我们有几种选择。

  • HTTP 长轮询 – 在这种情况下,客户端不断轮询附近朋友的位置。这种方法效率较低,因为轮询很可能会返回空结果多次。换句话说,可能会有很多空接收,这会浪费网络带宽。此外,在 HTTP 轮询的情况下,只有客户端可以轮询服务器,服务器无法主动将更新推送给客户端。

  • Web Sockets – 另一种选择是 Web Sockets。在这种情况下,客户端应用将始终与服务器连接,并且通信是双向的。客户端可以与服务器进行通信,服务器也可以将更新推送给客户端。这种方法的效率较高,因为服务器只会在用户的朋友有位置更新时才与客户端进行通信。

总体来说,Web Sockets 最适合我们的场景。Web Sockets 还有一个额外的优点,就是它们具有粘性会话。如果某个用户必须与特定的服务器实例建立连接,并且连接到服务器端的某个实例,那么他将始终连接到该实例。该用户的所有请求都将只发送到这个特定实例。因此,这使得 Web Sockets 成为点对点通信的一个好选择。

在本教程中,我们将讨论以下内容

  • 数据存储

  • 位置更新要求

  • 高级设计

  • API 设计

  • 位置共享的工作原理

  • 第一种方法 – 使用分布式 Redis

  • 第二种方法 – 使用 Redis Pub/Sub

  • 非功能需求

  • Redis Pub/Sub 服务器的可扩展性

  • 结论

数据存储

现在让我们分析需要存储的数据。

  • 用户档案:我们需要存储用户的个人资料信息,包括姓名、头像等。同时,我们需要保存与启用或禁用附近朋友相关的偏好设置。

  • 好友列表:系统应该能够存储所有用户的好友列表。该好友列表最终将用于显示附近的朋友。

  • 位置跟踪与存储:系统应能够跟踪用户的位置信息。同时,它们还应该能够存储位置历史记录。

  • 附近的朋友仅对在线用户可见。因此,系统应能够存储在线状态。

位置更新需求

关于附近朋友位置更新的工作原理,以下是一些需求

  • 当用户首次上线时,他应该能够看到所有在线的朋友

    • 在线用户

    • 且其当前位置在用户当前地点的 x 英里范围内

  • 当用户在线时,如果任何在线朋友的新位置在用户当前位置的x英里范围内,它将开始显示在附近的朋友列表中,反之亦然

  • 当用户下线时,他应该显示为离线状态,且其位置将不再共享。

因此,我们需要在设计中考虑三种场景。

  • 当用户首次上线时

  • 当用户在线时

  • 当用户下线时

高层设计

从高层次上讲,让我们讨论一下更高的流程以及所有需要存在的服务。

  • 将会有一个API 网关,所有来自用户的请求都将通过该网关。

  • 将会有一个会话服务会话服务将包含一组实例,用户将通过 WebSocket 连接到这些实例。由于每台机器可以打开的 WebSocket 数量有限,因此根据负载,用户的数量决定了需要多少台机器。

  • 会话服务将连接到一个分布式 Redis集群,该集群将包含哪些用户连接到哪个机器的信息。由于这些信息是临时的,直到用户断开连接,因此可以使用分布式 Redis来存储。这将由会话服务负责维护用户 ID 和机器 ID 的映射。此服务将

    • 如果有用户连接到任何一台机器,将插入记录。

    • 当用户断开连接时

  • 除此之外,会话将是一个“傻”服务,意思是它只接受连接,并将任何请求转发到一个SNS/Kafka主题。

  • 会话服务将在接收到任何用户活动时,向 SNS 主题或 Kafka 发布消息。

  • 将会有一个位置历史服务,用于保存用户的位置信息历史。将会有一个单独的表来存储位置历史,只有位置历史服务会连接到这个表。

  • 将会有一个用户最后一次查看服务,用于维护用户的最后查看时间和最后已知位置。

  • 将会有一个好友服务,用于维护所有用户的好友列表。将会有一个单独的表来存储好友列表,只有好友服务会连接到这个表。

  • 将会有一个用户档案服务,用于维护用户档案。它将有自己的一组表格来管理用户档案。

  • 将会有一个附近好友服务,负责管理附近的好友。它将处理用户在线、用户下线等情况。我们将在本教程中详细讨论此服务。

  • 将会有一个消息输出服务,它是一个工作服务,负责将外发消息发送给用户。该服务不会包含任何业务逻辑。它只会接收一条消息,消息中包含了需要发送的内容、发送对象以及用户连接的机器信息。这个服务非常简单,只处理消息,不包含任何业务逻辑。

  • 将会有一个令牌服务,用于管理用户的不同类型令牌。

API 设计

以下是所需的 API 列表:

  • 位置更新 API

  • 开启或关闭位置偏好设置

位置更新 API

当用户的位置发生变化时,客户端将调用位置更新 API。

请求体:

  • 经度

  • 纬度

  • 时间戳

开启或关闭位置偏好设置

当用户设置其位置偏好为开启或关闭时,将由客户端应用调用此接口。

请求体:

  • 用户 ID

  • 开/关

  • 时间戳

位置共享如何工作

关于位置共享,有两种方式。

  • 用户客户端应用将在定时间隔内拉取位置更新。

  • 好友的位置更新将推送到用户的客户端应用。

所以基本上就是拉取推送的区别。如我们之前所见,拉取模型的缺点是,它可能会浪费网络资源,因为很多时候接收到的可能是空数据。

Push的第二种方法在两个方面更好。

  • 只有当好友的实际位置发生变化时,更新才会被推送。

  • 推送方法将能更及时地反映正确的状态。因为拉取方法只能在固定时间间隔内执行,而时间间隔通常是以秒为单位。

我们将讨论两种实现推送位置更新的方法。

  • 在第一种方法中,我们将设计一个非常基础的方式。在此方法中,我们将使用分布式缓存。

  • 在第二种方法中,我们将使用 Redis 的发布/订阅(pub/sub)模式,并且我们将查看如何扩展这一方案

第一种方法 – 使用分布式 Redis

首先,让我们详细讨论一下为满足需求所需要的某些服务

位置历史 服务

将会有一个位置历史服务,它将连接到 location_history_db。这个数据库将存储用户的位置信息历史。以下是该数据库中的字段。我们可以使用任何 NoSQL 数据库,例如 MongoDB

  • id

  • user_id

  • location

  • 创建时间

  • 更新

用户最后一次出现 服务

这个服务将维护两项内容

  • 用户的 last_seen 时间

  • 用户的最后位置(如果适用)。

它将把这两项内容存储在 user_last_seen_db 中。以下是 user_last_seen_db 中的字段。我们也可以在这种情况下使用任何 NoSQL 数据库,例如 MongoDB

  • id

  • user_id

  • last_seen – 这是用户最后一次出现的时间戳

  • location – 只有在用户开启位置共享偏好 ON 时,这个字段才会被填充。

  • 创建时间

  • 更新

将会有一个新服务,负责管理附近的朋友。我们将这个服务命名为 nearby_friends 服务。

为了维护附近的朋友,我们将使用分布式缓存。这个分布式缓存也可以是 Redis 缓存。该缓存将维护所有打开位置偏好的用户的订阅列表。

举个例子,假设有两个用户 A、B 和 C。以下是朋友列表

  • A 和 B

  • A 和 C

  • A 和 D

以下键将在 Redis 中存在,其中

示例:

用户 A 的订阅列表

A : {
    B: 1,
    C: 1,
    D: 1
}

用户 B 的订阅列表

B : {
    A: 1
}

用户 C 的订阅列表

C : {
    A: 1
}

用户 D 的订阅列表

D : {
    A: 1
}

假设有三种场景,其中所有上述数据将被维护,并且整个附近朋友功能将如何工作。

  • 用户首次上线

  • 当用户已经在线时

  • 用户下线

我们将详细讨论这三种情况。但在深入细节之前,我们先假设

  • 用户 A 连接到节点 1

  • 用户 B 连接到节点 2

  • 用户 C 连接到节点 3

  • 用户 D 连接到节点 4

用户首次上线

我们将从用户 A 的角度描述这个场景。以下是用户首次上线时的情况图。假设用户 A 上线并连接到会话服务的节点 1。

附近朋友高层设计

附近朋友高层设计

然后节点 1 将把消息发布到 Kafka/SNS+ SQS 系统,消息包含当前位置和用户 ID。该消息将被三种不同的服务处理。

  • location_history 服务。

  • user_last_seen 服务。

  • nearby_friends 服务

location_history 服务将会更新 location_history_database 中用户 A 的当前位置。

user_last_seen 服务将更新 user_last_seen_db 中用户的最后一次见面时间。

nearby_friends 服务将执行以下操作。

  • 它将会获取所有启用了位置共享的用户 A 的朋友。假设用户 A 有三位启用了位置共享的朋友:BCD

  • 它将针对每个朋友 BCD 使用不同的 message_type 分发消息。

让我们跟踪发布给用户 B 的消息流程。

  • 用户 B 的消息将再次被 nearby_friends 服务处理。nearby_friends 服务将首先检查用户 B 是否在线,查询 user_last_seen 服务。如果 B 在线,user_last_seen 服务还将返回用户的当前位置。

  • 接着,附近朋友服务将计算用户 B 与用户 A 之间的距离。如果距离小于阈值,则将 B 添加为 A 的订阅者。它将创建如下的 Redis 条目。基本上,它将把用户 B 添加为 A 的订阅者。

A : {
    B: 1
}
  • 它还将把用户 A 添加为用户 B 的订阅者。以下 Redis 条目也将被创建。
B : {
    A: 1
}
  • 然后它将向 Kafka/SNS +SQS 系统发布两条消息,一条用于通知用户 A,一条用于通知用户 B

    • 第一条消息将由 message_outbound 服务处理,该服务将确定用户 B 连接的节点是节点 2。消息外发服务将调用节点 2,节点 2 将通知用户 B 的客户端应用程序用户 A 已上线。客户端应用程序将开始向用户 B 显示用户 A

    • 第二条消息也将由 message_outbound 服务处理,该服务将确定用户 A 连接的节点是节点 1。消息外发服务将调用节点 1,节点 1 将通知用户 A 的客户端应用程序用户 B 在阈值距离内。客户端应用程序将开始向用户 A 显示用户 B

对于用户 C 和 D 也会发生相同的事情。假设用户 C 的距离在 x 公里以内,而用户 D 的距离不在此范围内。在处理完用户 C 的消息后,将创建以下 Redis 条目。

用户 A 的订阅者列表。

A : {
    B: 1,
    C: 1
}

用户 B 的订阅者列表。

B : {
    A: 1
}

用户 C 的订阅者列表。

C : {
    A: 1
}

当用户已经在线时

用户 A 已经在线,但用户 A 的位置发生了变化。此时,新的位置信息将传递给节点 1。然后节点 1 将会将消息发布到 Kafka/SNS+ SQS 系统中,包含用户的当前位置。这个消息将会被三个不同的服务再次接收。

  • location_history 服务

  • user_last_seen 服务

  • nearby_friends 服务

location_historyuser_last_seen服务将分别更新用户的位置历史和最后一次见到的时间。

nearby_friends服务将会

  • 获取A的订阅者列表,这里是BC。然后,它将以不同的message_type将消息发送给BC。注意,它只会再次将消息发送给用户BC

  • 该消息将再次由nearby_friends服务接收,如果距离在阈值范围内,它将通知用户BC关于用户A的新位置,采用与用户首次上线时相同的方法,通过message_outbound工作者发送。

  • 如果位置不在阈值范围内,那么该用户将被从订阅者列表中移除,反之亦然。例如,假设AB的地点相距超过 x 公里。那么用户B对用户A的订阅以及用户A对用户B的订阅将被移除。

  • 用户A和用户B将通过message_outbound工作者被通知,表示对方不在范围内。

那么,如果假设朋友D已经在线,并且由于用户A的位置信息变化,朋友D现在位于 x 公里范围内,怎么办呢?那么朋友D应该开始在朋友A的列表中显示,反之亦然。

为了处理这种情况,我们将在每隔一段时间,比如 5 分钟(甚至更长时间),从用户A收到位置更新时,重新获取所有朋友的信息,并重新计算距离,以检查现有的在线朋友是否进入了范围。它将重复用户首次上线时提到的相同步骤。

假设这次用户D与用户A的距离小于 x 公里。那么以下的 redis 条目将被更新,用户A和用户D将被通知彼此的存在。

用户A的订阅者列表

A : {
    B: 1,
    C: 1,
    D: 1
}

用户B的订阅者列表

B : {
    A: 1
}

用户C的订阅者列表

C : {
    A: 1
}

用户D的订阅者列表

D : {
    A: 1
}

我们可以通过维护一个在(x + 10)公里范围内的朋友订阅列表来进一步优化上述场景,从而防止频繁移除和插入订阅者列表。

当然,订阅者列表会维护在(x + 10)公里的范围内,但只有当朋友之一位于 x 公里的阈值范围内时,才会发送通知。

当用户下线或关闭在线状态时

当用户下线或关闭在线状态时,他的在线状态应该停止向所有朋友显示。让我们逐一讨论这两种情况。

当用户下线时

以下图表展示了用户下线时的流程。

附近朋友下线情况

附近朋友下线情况

假设用户A下线。用户A的朋友BCD在用户A下线时仍然在线。

以下是即将发生的步骤顺序:

  • 用户A连接到会话服务的节点 1。一旦 WebSocket 连接被终止,连接终止所在的节点或机器将向 Kafka/SNS + SQS 系统发布一条消息。

  • nearby_friends 服务将接收消息。

  • 首先,该服务将从 Redis 分布式缓存中获取用户A的所有当前订阅者。返回的当前订阅者将是BCD

  • 然后它将为用户BCD分别发送三条消息。这些消息将被发布到 Kafka/SNS + SQS 系统。

  • 会有不同的消息,每条消息都会被nearby_friends服务接收。它将从用户BCD的相应广播消息中移除用户A的订阅。然后它将发布到message_outbound 服务。

  • message_outbound 服务将把消息转发给用户BCD,通知他们用户A已经下线。

当用户关闭在线状态时

这与用户下线时的情况相同。

第二种方法 – 使用 Redis Pub/Sub

现在我们将详细讨论 Redis Pub/Sub 方法。首先,什么是 Redis Pub/Sub?Redis Pub/Sub 允许

  • 命名频道的创建

  • 发布者可以向该命名频道发布消息。

  • 任何数量的订阅者都可以从该频道收听此消息。发布者发布的任何消息都将提供给所有订阅者。

您可以在这里阅读更多关于 Redis Pub/Sub 的内容 – redis.io/docs/manual/pubsub/

所以在这种方法中,我们将为每个用户维护一个单独的命名频道。所有在阈值距离内的在线朋友都将订阅该命名频道。

为了简单起见,假设特定用户的命名频道将使用该用户的 user_id 创建。

让我们再次考虑之前讨论的三种情况。

当用户第一次上线时

假设用户A上线并连接到会话服务的节点 1。

节点 1 首先将在 Redis 分布式缓存中为用户 A 创建一个命名频道(如果尚不存在)。然后,节点 1 将发布包含用户当前位置的消息到 Kafka/SNS + SQS 系统。节点 1 还将缓存 A 的最新位置。此消息将被三个不同的服务获取。

  • location_history 服务

  • user_last_seen 服务

  • nearby_friends 服务

location_history服务将更新用户 A 在 location_history_database 中的当前位置。

user_last_seen服务将更新用户在 last_seen 数据库中的最后一次看到时间。

nearby_friends服务将执行以下操作:

  • 它将从friends服务中获取所有启用了位置共享偏好的用户A的朋友。假设用户 A 有三位朋友BCD,他们都启用了位置共享。

  • 对于每个朋友,它将以不同的消息类型分发消息。因此,将分别为用户BCD发布三条不同的消息。

让我们跟踪发布给用户 B 的消息流程。

  • 用户 B 的消息将由nearby_friends服务再次获取。nearby_friends服务将首先检查用户 B 是否在线,通过user_last_seen服务。如果在线,user_last_seen服务还将返回用户的当前位置。

  • 然后,nearby_friends服务将计算自己与用户A的距离。如果距离小于阈值,它将向 Kafka/SNS + SQS 系统发布两条消息。

    • 这条首条消息将由message_outbound服务获取,后者将确定用户B连接的节点,即节点 2。消息外发服务将调用节点 2。节点 2 将用户B订阅到用户A的命名频道并开始监听。同时,节点 2 将通知用户B的客户端应用程序,A 已上线。客户端应用程序将开始将用户A展示给用户B。此外

    • 第二条消息也会由message_outbound服务获取,后者将确定用户 A 连接的节点,即节点 1。消息外发服务将调用节点 1。节点 1 会通知用户 A 的客户端应用程序,B 在阈值距离内。同时,节点 1 将用户A订阅到用户B的命名频道并开始监听。客户端应用程序将开始将用户B展示给用户A

用户C和用户D也会发生同样的事情,但假设用户D距离用户A的阈值 x 公里之外,因此用户D不会显示给用户A

总体来说

  • 用户A连接的节点 1 已订阅用户BC的命名频道。

  • 用户B连接的节点 2 已订阅用户A的命名频道。

  • 用户C连接的节点 3 已订阅了用户A的命名频道。

当用户已经在线时

用户A的位置发生变化。调用会到达节点 1,节点 1 将把新位置发布到用户A的命名频道。

新位置将被节点 2 和节点 3 分别接收到,并通知用户 B 和 C。两者都会重新计算距离,并

  • 如果距离小于 x 公里,则会通知BC用户A的新位置。

  • 如果距离超过 x 公里,则首先会从用户A的命名频道中移除BC的订阅。同时,系统会通知BC,用户A不在范围内。

节点 1 还会将新位置发布到 SNS/Kafka + SQS 系统,消息会被

  • location_history 服务

  • last_seen 服务

  • nearby_friends - 它将获取所有朋友并重新计算距离,以应对这种情况,即两位用户曾经在线,现在他们的位置已在 x 公里范围内。

location_historyuser_last_seen服务将会做与之前相同的操作。

当用户下线或关闭在线状态时

当用户下线或关闭在线状态时,他的在线状态应停止显示给所有朋友。我们将逐一讨论这两种情况。

当用户下线时

假设用户A下线。当用户A下线时,用户A的朋友BC正在线。以下是将要发生的步骤顺序:

调用会到达与用户A连接的节点 1。当节点 1 上的 WebSocket 连接被终止时,连接结束的节点或机器会在用户A的命名频道上发送一条消息,消息如下,表示用户已下线。

location:nil

节点 2 和节点 3,分别对应用户BC,会监听到这个消息,并立即通知BC用户A已下线。BC的客户端应用会立刻停止显示用户A

除此之外,还会将BC从用户A的命名频道中移除作为订阅者。

优化:通过nearby_friends服务以同步方式异步地移除BC的订阅也可以实现。

当用户关闭在线状态时

这与用户下线时的情况相同。

两种方法的比较

第二种方法显然稍微更快,因为会话服务中的所有节点或机器直接监听 redis 中的 pub/sub 频道,一旦消息发布,订阅者会立即收到通知。

但是也有一个缺点。如果用户数量如此庞大,则可能会有数百万或数十亿个命名频道。此外,对于如此数量的频道,可能会有 n 次订阅,其中 n 是每次在线的平均好友数。

所有这些订阅需要在会话服务的节点上维护,这可能会导致扩展困难。稍后,我们将进行容量估算,并讨论发布/订阅模型的扩展挑战。

非功能性需求

可扩展性

在上述设计中,首先需要考虑的是可扩展性因素。系统中每个组件的可扩展性非常重要。以下是你可能面临的可扩展性挑战及其可能的解决方案

  • 会话服务中的每台机器只能容纳有限数量的连接。因此,基于当前在线用户的数量,可以设置机器数量和实例数量。例如,一台机器大约有 65000 个端口。

  • 对于第二种方法,会话服务中的每台机器也可能会有数百万个对 Redis 发布/订阅的订阅。例如,如果一台机器通过 WebSocket 连接了 1000 个用户,而每个用户有 200 个在线好友。那么该机器将有 1000 * 200 = 200K 个订阅。这些订阅将会对内存和 CPU 产生影响。扩展会话服务时,必须考虑这一影响。

  • 对于分布式缓存的可扩展性,我们应该启用水平扩展。稍后在本教程中,我们将详细讨论 Redis 发布/订阅的分布式缓存扩展。

  • 你的 Kafka 系统可能无法承载如此大的负载。我们可以进行水平扩展,但有其限制。如果成为瓶颈,那么根据地理位置或用户 ID,我们可以部署两个或更多这样的系统。可以使用服务发现来确定请求应该发送到哪个 Kafka 系统。

  • 对其他服务也可以采取类似的方法。

  • 另一个可扩展性的重要因素是我们已经设计了系统,以确保没有任何服务被过多任务拖累。系统之间有清晰的责任分离,对于那些负担过重的服务,我们进行了拆分。

Redis 发布/订阅服务器的可扩展性

正如我们之前提到的,Redis 发布/订阅频道创建成本很低。Redis 发布/订阅能够处理大量频道,因此可以支持大量发布者和订阅者。

Redis 发布/订阅的可扩展性因素有哪些?

  • 频道数量

  • Redis 发布/订阅集群中使用的实例类型

    • 内存

    • CPU

  • 集群中的实例数量或集群的水平扩展性。

  • 发布到频道的消息的平均大小

  • 集群的复制

频道数量

我们来做一些基于用户估算的数学假设。假设有 10 亿个用户,其中 20%的用户始终在线,并启用了位置偏好设置。也就是说,200 百万用户。假设每个用户平均有 20 个朋友同时在线。所以总体上我们将需要

  • 需要创建 2 亿个频道

  • 每个频道将有 20 个订阅者

Redis 发布/订阅集群中使用的实例类型

Redis 发布/订阅运行所在的硬件是一个重要因素,主要是机器的内存和 CPU。机器的内存和 CPU 越多,可以创建的频道就越多。

内存

每个用户的 UUID 为 16 字节。每个频道将有 200 个订阅者。Redis 发布/订阅频道是通过哈希表和双向链表在内部实现的。

总体大小大约为

  • 16 字节 UUID * 1 个发布者 = 16 字节

  • 16 字节的 UUID * 200 个订阅者 = 3200 字节

  • 8 字节指针 * 21 = 180 字节

所以每个频道大约需要 3400 字节。我们可以假设每个频道平均占用 4000 字节或 4KB。

2 亿个频道 * 4 KB = 800 GB

如果每台服务器的内存平均为 20 GB,我们大约需要 800/20=40 台服务器作为最小配置。同时,还需要考虑到这一点。

CPU

集群的 CPU 需求必须通过负载测试来计算。根据负载测试的结果,可能需要增加或减少服务器数量。

### 集群中的实例数量或集群的水平可扩展性

集群必须具备水平可扩展性,这意味着如果需要,可以添加新服务器;如果不需要,可以删除现有服务器。我们可以使用一致性哈希来实现这一点。使用一致性哈希时,新增或删除服务器时,键的分布会最小化。

发布到频道的消息的平均大小

这是考虑可扩展性时需要注意的一个重要因素,因为当消息发布到频道时,Redis 发布/订阅将会一直保存消息,直到所有订阅者接收到该消息。

集群的复制

集群还将启用复制。集群中的每个实例也会有副本。

上述可扩展性考虑同样适用于我们将在方法 1 中使用的分布式缓存。

结论

这就是关于设计附近朋友的内容。希望你喜欢这篇文章。请在评论中分享反馈。

备注: 查看我们的系统设计教程系列 系统设计问题**

国际象棋游戏系统设计

原文:techbyexample.com/chess-low-level-design/

目录

  • 概述

  • 需求

  • 游戏中的所有参与者

  • UML 图

    • 棋子

    • 格子

    • 移动

    • 玩家

    • 棋盘

    • 游戏类

  • 低级图

  • 代码

  • 结论

概述

在本教程中,我们将设计一款国际象棋游戏。国际象棋是一种在两名玩家之间进行的棋盘游戏。关于国际象棋的更多信息可以在此处找到

en.wikipedia.org/wiki/Chess

在开始之前,请注意这是一个低级设计问题。我们将使用面向对象原则设计一款国际象棋游戏。

设计一款国际象棋游戏是一个非常常见的系统设计面试问题,和其他任何系统设计面试一样,面试官关注的是:

  • 你对面向对象设计的理解

  • 你如何在设计中框架设计模式

另外,请注意此问题不是分布式系统设计。

在本教程中,我们将讨论国际象棋游戏的低级设计。与此同时,我们还将查看国际象棋游戏的工作代码。以下是目录:

  • 需求

  • 设计中的参与者

  • UML 图

  • 用 Go 编程语言表示的低级设计

  • 工作代码

需求

本教程的需求是根据国际象棋规则,使用面向对象设计原则设计一款国际象棋游戏。

这篇文章可以参考了解更多关于国际象棋规则的信息

en.wikipedia.org/wiki/Rules_of_chess

下一步是识别国际象棋游戏中的所有参与者

游戏中的所有参与者

  • 棋盘

  • 玩家

    • 人类玩家

    • 计算机玩家

  • 移动

  • 棋子

    • 国王

    • 后宫

    • 主教

  • 游戏

  • 移动

让我们来看一下这些参与者

  • 棋盘 – 在国际象棋游戏中,我们有一个 8*8 的棋盘。所以总共有 64 个格子。

  • 格子 – 一个格子表示 8*8 = 64 个格子中的一个

  • 玩家 – 它表示正在进行游戏的两名玩家之一。一个玩家可以是:

    • 人类玩家 –

    • 计算机玩家

  • 移动 – 它表示玩家之一所做的移动

  • 棋子 – 国际象棋游戏中有不同类型的棋子,分别是:

    • 国王

    • 后宫

    • 主教

  • 游戏 – 游戏类控制着整个国际象棋游戏的流程。它决定了当前是哪个玩家的回合,游戏的总体结果等等。

  • 移动 – 它简单地表示游戏中的一次移动。

除了上述的参与者外,我们还有 游戏状态 枚举,其中包含以下类型:

  • 第一玩家胜

  • 第二玩家胜

  • 游戏进行中

  • 和棋

  • 和棋

UML 图

以下是国际象棋游戏的 UML 图

让我们通过查看不同的组件以及每个组件如何与其他组件集成,来理解这个 UML 图。

棋子

上述设计中最简单的组件是棋子。如上所述,棋子有七种类型,分别是国王、皇后、车、象、骑士和兵。棋子类是一个抽象类,包含以下信息

  • 白色 – 如果棋子是白色,则设置为真,否则为假

  • 已被捕获 – 棋子是否被吃掉。

此外,它还提供了一个函数canMove(),如果给定的棋子可以从当前棋盘位置移动到新位置,则返回真。该函数的签名如下

  • canMove(board, location, location)

棋子

下一个最简单的组件是格子。格子代表棋盘上的一个格子。一个格子将具有以下组件

  • 位置 – 它表示棋盘上格子的xy坐标

  • 棋子 – 当前在指定格子上的棋子。如果该格子没有棋子,则为 nil

移动

它代表棋局中的一次移动。国际象棋中有三种不同的移动方式。

  • 认输

  • 和棋提议

  • 将棋子从当前位置移动到新的格子

它包含以下组件,代表上述三种类型的移动。

  • 当前所在位置 – 棋子当前的位置

  • 新位置 – 棋子移动到的新位置

  • 棋子 – 被移动的棋子

  • 认输 – 这步是认输还是不是

  • 和棋提议 – 玩家是否提出和棋提议。

玩家

有两种不同类型的玩家

  • 人类玩家

  • 计算机玩家

玩家将拥有以下字段或组件

  • isWhite – 玩家是否执白棋

  • getNextMove – 获取玩家的下一步

  • agreeDraw – 玩家是否同意和棋提议

棋盘

接下来是棋盘类。它代表游戏的棋盘。它包含以下组件

  • 方格 – 二维数组格子

  • 维度 – 棋盘的维度,始终为 8

游戏类

接下来是系统中最重要的类——游戏类。它包含以下组件

  • 棋盘 – 它代表棋盘

  • 先手玩家 – 先手玩家

  • 第二玩家 – 第二个玩家

  • 先手玩家回合 – 如果当前是先手玩家的回合则为真,否则为假

  • 游戏状态 – 当前游戏的状态

  • 已移动 – 迄今为止所有已做出的移动

低级级别图

下面是 Go 编程语言中的低级图

游戏

type game struct {
	board           *board
	firstPlayer     iPlayer
	secondPlayer    iPlayer
	firstPlayerTurn bool
	gameStatus      GameStatus
	moves           []move
}

func (this *game) play() error {}

func (g *game) setGameStatus(win bool, whiteWon bool, stalemate bool) {}

func (g *game) printResult() {}

游戏状态

type GameStatus uint8

const (
    GameInProgress GameStatus = iota
    GameDraw
    Stalemate
    FirstPlayerWin
    SecondPlayerWin
)

棋盘

type board struct {
	square    [][]cell
	dimension int
}

func (this *board) printBoard() {}

func (this *board) makeMove(move move) error {}

格子

type cell struct {
	location location
	piece    piece
}

移动

type move struct {
    currLocation location
    newLocation  location
    piece        piece
    resign       bool
    drawOffer    bool
}

iPlayer 接口

type iPlayer interface {
    isWhite() bool
    getNextMove(board) move
    agreeDraw(board) bool
    getID() int
}

人类玩家

type humanPlayer struct {
    white bool
    id    int
}

func (this humanPlayer) getID() int {}

func (this humanPlayer) isWhite() bool {}

func (this humanPlayer) getNextMove(board board) move {}

func (this humanPlayer) agreeDraw(board board) bool {}

计算机玩家

type computerPlayer struct {
	white bool
	id    int
}

func (this computerPlayer) isWhite() bool {}

func (this computerPlayer) getID() int {}

func (this computerPlayer) getNextMove(board board) move {}

func (this computerPlayer) agreeDraw(board board) bool {}

棋子

type piece interface {
	canMove(board, location, location) error
	getPieceType() PieceType
	isKilled() bool
	isWhite() bool
}

棋子类型

type PieceType uint8

const (
	King PieceType = iota
	Queen
	Rook
	Bishop
	Knight
	Pawn
)

国王棋子

type king struct {
	white  bool
	killed bool
}

func (this *king) canMove(board board, currLocation, newLocation location) error {}

func (this *king) getPieceType() PieceType {}

func (this *king) isKilled() bool {}

func (this *king) isWhite() bool {}

皇后棋子

type queen struct {
	white  bool
	killed bool
}

func (this *queen) canMove(board board, currLocation, newLocation location) error {}

func (this *queen) getPieceType() PieceType {}

func (this *queen) isKilled() bool {}

func (this *queen) isWhite() bool {}

车棋子

type rook struct {
	white  bool
	killed bool
}

func (this *rook) canMove(board board, currLocation, newLocation location) error {}

func (this *rook) getPieceType() PieceType {}

func (this *rook) isKilled() bool {}

func (this *rook) isWhite() bool {}

象棋子

type bishop struct {
	white  bool
	killed bool
}

func (this *bishop) canMove(board board, currLocation, newLocation location) error {}

func (this *bishop) getPieceType() PieceType {}

func (this *bishop) isKilled() bool {}

func (this *bishop) isWhite() bool {}

骑士棋子

type knight struct {
	white  bool
	killed bool
}

func (this *knight) canMove(board board, currLocation, newLocation location) error {}

func (this *knight) getPieceType() PieceType {}

func (this *knight) isKilled() bool {}

func (this *knight) isWhite() bool {}

兵棋子

type pawn struct {
	white  bool
	killed bool
}

func (this *pawn) canMove(board board, currLocation, newLocation location) error {}

func (this *pawn) getPieceType() PieceType {}

func (this *pawn) isKilled() bool {}

func (this *pawn) isWhite() bool {}

位置

type location struct {
	i int
	j int
}

代码

让我们来看看代码。下面的代码并不是根据国际象棋算法和规则完全可用的功能性代码,但它可以给你一个大致的概念。

game.go

package main

import "fmt"

type game struct {
	board           *board
	firstPlayer     iPlayer
	secondPlayer    iPlayer
	firstPlayerTurn bool
	gameStatus      GameStatus
	moves           []move
}

func initGame(b *board, p1, p2 iPlayer) *game {
	game := &game{
		board:           b,
		firstPlayer:     p1,
		secondPlayer:    p2,
		firstPlayerTurn: true,
		gameStatus:      GameInProgress,
	}
	return game
}

func (this *game) play() error {
	var move move
	var err error
	for {
		if this.firstPlayerTurn {
			move = this.firstPlayer.getNextMove(*this.board)
			if move.resign {
				this.setGameStatus(true, true, false)
			}
			if move.drawOffer {
				if this.secondPlayer.agreeDraw(*this.board) {
					this.setGameStatus(false, true, false)
					break
				}
			}
			err := this.board.makeMove(move)
			if err != nil {
				return err
			}
		} else {
			move = this.firstPlayer.getNextMove(*this.board)
			if move.resign {
				this.setGameStatus(true, true, false)
			}
			if move.drawOffer {
				if this.secondPlayer.agreeDraw(*this.board) {
					this.setGameStatus(false, true, false)
					break
				}
			}
			err = this.board.makeMove(move)
			if err != nil {
				return err
			}
		}
		this.moves = append(this.moves, move)
		win, draw, stalemate := this.checkGameStatus()
		this.setGameStatus(win, draw, stalemate)
		if this.gameStatus != GameInProgress {
			break
		}
	}
	return nil
}

func (this *game) checkGameStatus() (win bool, whiteWon bool, stalemate bool) {

	return true, true, true

}

func (g *game) setGameStatus(win bool, whiteWon bool, stalemate bool) {
	if win {
		if whiteWon {
			if g.firstPlayer.isWhite() {
				g.gameStatus = FirstPlayerWin
				return
			} else {
				g.gameStatus = SecondPlayerWin
				return
			}
		}
	}
	if stalemate {
		g.gameStatus = Stalemate
	}
	g.gameStatus = GameDraw
	return
}

func (g *game) printResult() {
	switch g.gameStatus {
	case GameInProgress:
		fmt.Println("Game in Progress")
	case GameDraw:
		fmt.Println("Game Drawn")
	case Stalemate:
		fmt.Println("Stalemate")
	case FirstPlayerWin:
		fmt.Println("First Player Win")
	case SecondPlayerWin:
		fmt.Println("Second Player Win")
	default:
		fmt.Println("Invalid Game Status")
	}
	g.board.printBoard()
}

gamestatus.go

package main

type GameStatus uint8

const (
	GameInProgress GameStatus = iota
	GameDraw
	Stalemate
	FirstPlayerWin
	SecondPlayerWin
)

board.go

package main

type board struct {
	square    [][]cell
	dimension int
}

func (this *board) printBoard() {

}

func (this *board) makeMove(move move) error {
	err := move.piece.canMove(*this, move.currLocation, move.newLocation)
	if err != nil {
		return err
	}

	//Mark the piece at new location as as the current piece
	this.square[move.newLocation.i][move.newLocation.j].piece = move.piece

	//Mark the piece at current location as nil
	this.square[move.currLocation.i][move.currLocation.j].piece = nil
	return nil
}

cell.go

package main

type cell struct {
	location location
	piece    piece
}

move.go

package main

type move struct {
	currLocation location
	newLocation  location
	piece        piece
	resign       bool
	drawOffer    bool
}

iPlayer.go

package main

type iPlayer interface {
	isWhite() bool
	getNextMove(board) move
	agreeDraw(board) bool
	getID() int
}

humanPlayer.go

package main

type humanPlayer struct {
	white bool
	id    int
}

func (this humanPlayer) getID() int {
	return this.id
}

func (this humanPlayer) isWhite() bool {
	return this.white
}

func (this humanPlayer) getNextMove(board board) move {
	currLocation := location{
		i: 1,
		j: 0,
	}

	newLocation := location{
		i: 2,
		j: 0,
	}
	return move{
		currLocation: currLocation,
		piece:        board.square[1][0].piece,
		newLocation:  newLocation,
	}
}

func (this humanPlayer) agreeDraw(board board) bool {
	return false
}

computerPlayer.go

package main

type computerPlayer struct {
	white bool
	id    int
}

func (this computerPlayer) isWhite() bool {
	return this.white
}
func (this computerPlayer) getID() int {
	return this.id
}

func (this computerPlayer) getNextMove(board board) move {
	currLocation := location{
		i: 1,
		j: 0,
	}

	newLocation := location{
		i: 2,
		j: 0,
	}
	return move{
		currLocation: currLocation,
		piece:        board.square[1][0].piece,
		newLocation:  newLocation,
	}
}

func (this computerPlayer) agreeDraw(board board) bool {
	return false
}

piece.go

package main

type piece interface {
	canMove(board, location, location) error
	getPieceType() PieceType
	isKilled() bool
	isWhite() bool
}

pieceType.go

package main

type PieceType uint8

const (
	King PieceType = iota
	Queen
	Rook
	Bishop
	Knight
	Pawn
)

king.go

package main

type king struct {
	white  bool
	killed bool
}

func (this *king) canMove(board board, currLocation, newLocation location) error {
	return nil
}

func (this *king) getPieceType() PieceType {
	return King
}

func (this *king) isKilled() bool {
	return this.killed
}

func (this *king) isWhite() bool {
	return this.white
}

queen.go

package main

type queen struct {
	white  bool
	killed bool
}

func (this *queen) canMove(board board, currLocation, newLocation location) error {
	return nil
}

func (this *queen) getPieceType() PieceType {
	return Queen
}

func (this *queen) isKilled() bool {
	return this.killed
}

func (this *queen) isWhite() bool {
	return this.white
}

rook.go

package main

type rook struct {
	white  bool
	killed bool
}

func (this *rook) canMove(board board, currLocation, newLocation location) error {
	return nil
}

func (this *rook) getPieceType() PieceType {
	return Pawn
}

func (this *rook) isKilled() bool {
	return this.killed
}

func (this *rook) isWhite() bool {
	return this.white
}

bishop.go

package main

type bishop struct {
	white  bool
	killed bool
}

func (this *bishop) canMove(board board, currLocation, newLocation location) error {
	return nil
}

func (this *bishop) getPieceType() PieceType {
	return Pawn
}

func (this *bishop) isKilled() bool {
	return this.killed
}

func (this *bishop) isWhite() bool {
	return this.white
}

knight.go

package main

type knight struct {
	white  bool
	killed bool
}

func (this *knight) canMove(board board, currLocation, newLocation location) error {
	return nil
}

func (this *knight) getPieceType() PieceType {
	return Knight
}

func (this *knight) isKilled() bool {
	return this.killed
}

func (this *knight) isWhite() bool {
	return this.white
}

pawn.go

package main

type pawn struct {
	white  bool
	killed bool
}

func (this *pawn) canMove(board board, currLocation, newLocation location) error {
	return nil
}

func (this *pawn) getPieceType() PieceType {
	return Pawn
}

func (this *pawn) isKilled() bool {
	return this.killed
}

func (this *pawn) isWhite() bool {
	return this.white
}

location.go

package main

type location struct {
	i int
	j int
}

main.go

package main

func main() {

	whiteKing, whiteQueen, whiteRooks, whiteBishops, whiteKnights, whitePawns := makePieces(true)
	blackKing, blackQueen, blackRooks, blackBishops, blackKnights, blackPawns := makePieces(true)

	cells := make([][]cell, 8)

	for i := 0; i < 8; i++ {
		cells[i] = make([]cell, 8)
	}

	//Fill White Pieces in the first row
	cells[0][0] = cell{location: location{i: 0, j: 0}, piece: whiteRooks[0]}
	cells[0][1] = cell{location: location{i: 0, j: 1}, piece: whiteKnights[0]}
	cells[0][2] = cell{location: location{i: 0, j: 2}, piece: whiteBishops[0]}
	cells[0][3] = cell{location: location{i: 0, j: 3}, piece: whiteKing}
	cells[0][4] = cell{location: location{i: 0, j: 4}, piece: whiteQueen}
	cells[0][5] = cell{location: location{i: 0, j: 5}, piece: whiteBishops[1]}
	cells[0][6] = cell{location: location{i: 0, j: 6}, piece: whiteKnights[1]}
	cells[0][7] = cell{location: location{i: 0, j: 7}, piece: whiteRooks[1]}
	//Fill White Pawns in the second row
	for i := 0; i < 8; i++ {
		cells[1][i] = cell{location: location{i: 0, j: 7}, piece: whitePawns[i]}
	}

	//Fill Black Pieces in the first row
	cells[7][0] = cell{location: location{i: 7, j: 0}, piece: blackRooks[0]}
	cells[7][1] = cell{location: location{i: 7, j: 1}, piece: blackKnights[0]}
	cells[7][2] = cell{location: location{i: 7, j: 2}, piece: blackBishops[0]}
	cells[7][3] = cell{location: location{i: 7, j: 3}, piece: blackKing}
	cells[7][4] = cell{location: location{i: 7, j: 4}, piece: blackQueen}
	cells[7][5] = cell{location: location{i: 7, j: 5}, piece: blackBishops[1]}
	cells[7][6] = cell{location: location{i: 7, j: 6}, piece: blackKnights[1]}
	cells[7][7] = cell{location: location{i: 7, j: 7}, piece: blackRooks[1]}
	//Fill Black Pawns in the second row
	for i := 0; i < 8; i++ {
		cells[6][i] = cell{location: location{i: 0, j: 7}, piece: blackPawns[i]}
	}

	board := &board{
		square:    cells,
		dimension: 8,
	}

	player1 := humanPlayer{
		white: true,
		id:    1,
	}

	player2 := computerPlayer{
		white: false,
		id:    1,
	}

	game := initGame(board, player1, player2)
	game.play()
	game.printResult()
}

func makePieces(isWhite bool) (*king, *queen, [2]*rook, [2]*bishop, [2]*knight, [8]*pawn) {

	king := &king{
		white: isWhite,
	}

	queen := &queen{
		white: isWhite,
	}

	rooks := [2]*rook{}
	for i := 0; i < 2; i++ {
		r := &rook{
			white: true,
		}
		rooks[i] = r
	}

	bishops := [2]*bishop{}
	for i := 0; i < 2; i++ {
		b := &bishop{
			white: true,
		}
		bishops[i] = b
	}

	knights := [2]*knight{}
	for i := 0; i < 2; i++ {
		k := &knight{
			white: true,
		}
		knights[i] = k
	}

	pawns := [8]*pawn{}
	for i := 0; i < 8; i++ {
		p := &pawn{
			white: isWhite,
		}
		pawns[i] = p
	}

	return king, queen, rooks, bishops, knights, pawns
}

结论

这就是关于国际象棋游戏的低级设计。我们希望你喜欢这篇文章。请在评论中分享反馈。

设计一个点击计数器

原文:techbyexample.com/system-design-hit-counter/

概述

目标是设计一个点击计数器,能够记录网站在过去 5 分钟内的访问次数。这是一个棘手的问题。你的设计应能够回答以下一般性查询

  • 过去 n 分钟内的点击次数,其中 1 <= n <= 5

本教程中描述的解决方案也可以扩展到大于 5 分钟的情况。

思路是设计一个数据结构,能够记录过去五分钟内每一分钟的点击次数。

为此,我们可以使用以下数据结构

  • 计数器数组 – 一个长度为 5 的整数数组。

  • 时间戳数组 – 一个长度为 5 的整数数组。

Counter[i] 将存储第 i 分钟内的点击次数

Timestamp[i] 将存储第 i 分钟内最后一次点击的时间戳。它将是一个表示该分钟内的纪元时间戳的数字。

以下是算法

  • 获取给定时间戳的纪元时间戳(以分钟为单位)。我们将其称为 timestamp_epoch

  • 将该纪元时间戳除以 5 并取余。这个余数将被称为 I

  • 如果 timestamp[i] = timestamp_epoch,那么这意味着第 i 分钟的上一条点击发生在同一分钟。因此,增加计数器。基本上执行 counter[i]++

  • 如果 timestamp[i] != timestamp_epoch,那么这意味着第 i 分钟的最后一次点击发生在不同的分钟。因此,我们需要重置计数器。基本上执行 counter[i] = 1

程序

以下是相应的代码

package main

import "fmt"

var (
	counter   [5]int
	timestamp [5]int
)

func recordHit(timestamp_epoch int) {

	i := timestamp_epoch % 5

	if timestamp[i] != timestamp_epoch {
		counter[i] = 1
		timestamp[i] = timestamp_epoch
	} else {
		counter[i]++
	}

	fmt.Printf("After hit at time: %d\n", timestamp_epoch)
	fmt.Println(counter)
	fmt.Println(timestamp)
	fmt.Println()

}

func getNumberOfHit(timestamp_epoch int, minute int) (int, error) {

	if minute > 5 {
		return 0, fmt.Errorf("incorrect value of past minute passed")
	}
	hits := 0

	for k := timestamp_epoch; k > timestamp_epoch-minute; k-- {
		i := k % 5
		if timestamp[i] > timestamp_epoch-5 {
			hits = hits + counter[i]
		}
	}

	return hits, nil

}
func main() {
	recordHit(1000)

	recordHit(1000)

	hits, _ := getNumberOfHit(1000, 1)
	fmt.Printf("Current timestamp: %d,Last minue: %d,  Hits:%d\n\n", 1000, 1, hits)

	recordHit(1001)

	hits, _ = getNumberOfHit(1001, 2)
	fmt.Printf("Current timestamp: %d, Last minue: %d, Hits:%d\n\n", 1001, 2, hits)

	recordHit(1005)

	hits, _ = getNumberOfHit(1005, 5)
	fmt.Printf("Current timestamp: %d, Last minue: %d,  Hits:%d\n", 1005, 5, hits)

	hits, _ = getNumberOfHit(1005, 2)
	fmt.Printf("Current timestamp: %d, Last minue: %d, Hits:%d\n", 1005, 2, hits)

	recordHit(1007)
	hits, _ = getNumberOfHit(1007, 5)
	fmt.Printf("Current timestamp: %d, Last minue: %d, Hits:%d\n", 1007, 5, hits)

} 

输出

After hit at time: 1000
[1 0 0 0 0]
[1000 0 0 0 0]

After hit at time: 1000
[2 0 0 0 0]
[1000 0 0 0 0]

Current timestamp: 1000,Last minue: 1,  Hits:2

After hit at time: 1001
[2 1 0 0 0]
[1000 1001 0 0 0]

Current timestamp: 1001, Last minue: 2, Hits:3

After hit at time: 1005
[1 1 0 0 0]
[1005 1001 0 0 0]

Current timestamp: 1005, Last minue: 5,  Hits:2
Current timestamp: 1005, Last minue: 2, Hits:1
After hit at time: 1007
[1 1 1 0 0]
[1005 1001 1007 0 0]

Current timestamp: 1007, Last minue: 5, Hits:2

让我们通过一个例子来理解它

  • 在开始时,counter[i] = 0 对于 i = 0 到 4。同样,timestamp[i] = 0 对于 i = 0 到 4
counter = [0, 0, 0, 0, 0]
timestamp = [0, 0, 0, 0, 0]
  • 假设当前的纪元时间戳(以分钟为单位)是 1000。我们将其称为 timestamp_epoch

  • 假设第一分钟有一次点击

    • i = 1000%5,结果为 0

    • timestamp[i] 为 0。因此,timestamp[i] 不等于 timestamp_epoch

    • 执行 counter[i]++,这意味着 counter[0] = 1

    • timestamp[i] 设置为 1000,即当前的分钟时间戳。因此,counter 和 timestamp 将会是

counter = [1, 0, 0, 0, 0]
timestamp = [1000, 0, 0, 0, 0]
  • 假设在第一分钟内有另一个点击

    • i = 1000%5,结果为 0

    • timestamp[i] 为 1。因此,timestamp[i] 等于 timestamp_epoch

    • 执行 counter[i]++,这意味着 counter[0] = 2

    • 将 timestamp[0] 设置为 1000,即当前的分钟时间戳。因此,counter 和 timestamp 将会是

counter = [2, 0, 0, 0, 0]
timestamp = [1000, 0, 0, 0, 0]
  • 假设在第二分钟有另一次点击,当时该分钟的时间戳为 1001

    • i = 1001%5,结果为 1

    • timestamp[1] 当前为 0。因此,timestamp[1] 不等于 timestamp_epoch

    • 执行 counter[1]++,这意味着 counter[1] = timestamp_remainder

    • 将 timestamp[1] 设置为 1001,即 timestamp_epoch 对应的分钟时间戳。因此,counter 和 timestamp 将会是

counter = [2, 1, 0, 0, 0]
timestamp = [1000, 1001, 0, 0, 0]
  • 现在假设在第 6 分钟又有一次点击。此时,当前的纪元时间戳(以分钟为单位)将是 1005。为什么是 1005?因为 1000 是第一分钟,那么 1005 就是第六分钟)

    • i = 1005%5,结果为 0

    • timestamp[0] 是 1000。所以 timestamp[0] 不等于 timestamp_epoch

    • 重置计数器。将 counter[0] 设置为 1,这意味着 counter[0] = 1

    • 将 timestamp[0] 设置为 1005,这在分钟单位中是 timestamp_epoch。因此计数器和时间戳将是

counter = [1, 1, 0, 0, 0]
timestamp = [1005, 1001, 0, 0, 0]

在任何给定时刻,要获取每分钟的点击次数,我们可以简单地将计数器数组中的所有条目加起来。

结论

这就是设计 Hit Counter 的全部内容。希望你喜欢这篇文章。请在评论中分享反馈。

停车场系统设计

原文:techbyexample.com/parking-lot-system-design/

目录

  • 概述

  • 需求

  • 停车系统设计中的所有角色

    • 停车位

    • 停车门

    • 停车票

    • 支付网关类型

    • 车辆

    • 停车费

    • 停车楼层

    • 停车系统

  • UML 图

  • 低级设计

  • 程序

  • 完整工作代码在一个文件中

  • 结论

概述

在回答任何系统设计问题时,重要的是要记住系统设计问题可能非常广泛。因此,切勿直接跳到解决方案。与面试官讨论用例是很好的,这样可以理解他在寻找什么。决定在你的系统设计中包含哪些功能。

这也是面试官在考察的一个方面。他们可能在寻找

  • 你如何进行需求分析

  • 你能否列出所有的需求

  • 你在问什么问题?

设计一个停车场是一个非常常见的系统设计面试问题,和其他任何系统设计面试一样,面试官在寻找的就是这些

  • 你对面向对象设计的理解

  • 你如何在设计模式的框架下进行设计

另外,请注意,这个问题不是分布式系统设计问题。

在本教程中,我们将讨论停车场的低级设计。除此之外,我们还将展示停车场的完整工作代码。以下是目录

  • 需求

  • 设计中的角色

  • UML 图

  • 用 Go 编程语言表示的低级设计

  • 完整的工作代码

  • 完整工作代码在一个文件中

需求

设计任何东西时,了解系统的一些高级需求是至关重要的。

  • 系统可能有多个入口

  • 停车场中可能会有多个

  • 将有不同类型的停车位

  • 每小时停车将收取费用。费用将根据不同类型的停车位和不同的车辆类型进行收费

  • 系统应该能够适应任何类型的变化

  • 停车场可以是多层的

停车系统设计中的所有角色

  • 停车系统

  • 停车楼层

  • 停车位 – 不同类型的停车位

    • 停车位

    • 卡车停车位

    • 摩托车停车位

  • 停车票

  • 入口和出口门

  • 支付网关

  • 车辆

让我们来看看这些角色中的每一个

停车位

现在,停车位可以是不同类型的。它可以是:

  • 汽车停车位

  • 大型车辆停车位

  • 摩托车停车位

停车闸口

可能会有进出口闸口。不同楼层可能会有不同的闸口。

停车票

每辆进场的车辆都会发放停车票。车辆将根据类型分配停车位。例如,汽车将被分配到汽车停车位类型,摩托车将被分配到摩托车停车位,等等。

支付网关类型

客户可以通过以下方式支付:

  • 现金

  • 借记卡

  • 信用卡

车辆

停车场为以下车辆类型提供停车位

  • 汽车

  • 卡车

  • 摩托车

停车费

停车一辆车、卡车或摩托车的费用是不同的。这些将被封装在停车费用类别中。

停车楼层

停车楼层维护该楼层所有停车位的列表。停车楼层将负责预定和释放该楼层不同类型的停车位。

停车系统

它是系统的主要组件,是我们设计中的驱动类。

这些角色如何相互通信将通过 UML 图和后续的解释来清楚展示。

UML 图

以下是停车场的 UML 图

让我们通过查看不同的组件,理解这个 UML 图以及每个组件是如何与其他组件集成的。

上述 UML 系统中最简单的组件是停车位。一个停车位包含三个信息:

  • 满位 – 表示停车位是否已满

  • 楼层 – 停车位所在的楼层

  • 位置 – 停车位的具体位置编号

下一个组件是停车票。停车票将包含以下信息:

  • 车辆 – 该停车票是为哪种类型的车辆发放的。车辆还会有车牌号码。

  • 停车位 – 车辆停放的具体停车位。你可能会想为什么停车票中需要包含这一信息。这个信息存在于停车票中是为了我们能够找回停车位。你可能会争辩说,一旦停车票发出,停车就可以停在任何位置。但在本教程中,我们正在构建一个更复杂的系统,在这个系统中,我们可以确切知道车辆停放的位置。如果你想构建一个系统,在其中针对某张停车票停车可以分配到任何停车位,那是比较简单的,方法是我们只需为每个停车位维护一个计数。事实上,这就是本教程中设计的简化版本。

  • 除此之外,停车票还会包含入场时间、出场时间、价格、闸口信息、PG 信息等。

下一个组件是停车楼层。停车楼层维护该楼层所有停车位的列表。它包含以下信息:

  • 停车位 – 每种类型的停车位都会有一个双向链表。例如,车位类型停车位会有一个单独的 DLL,而卡车类型停车位则会有不同的 DLL。为什么使用 DLL?使用 DLL 可以轻松在 O(1)时间内找到下一个空闲停车位。而且当一个停车位变为空闲时,我们可以简单地将其移到链表前面,表示它是空的。

  • 楼层编号 – 楼层编号仅仅是

  • isFull – 表示是否已满

然后,主要组件是ParkingSystem。它将包含以下信息

  • 一个停车楼层的数组

  • 它已经发出的停车票列表

  • 除此之外,它还会包含关于出入口门的信息,isFull 变量用于表示整个停车场是否已满。

除了这些主要组件,我们还有

  • 车辆组件

  • 支付网关组件

  • 出入口门

  • 停车费

  • 等等

此设计中使用的设计模式

  • 使用工厂模式创建不同类型停车位的实例

  • 使用享元设计模式创建固定的停车费和支付网关实例。

级设计

以下是用 Go 编程语言表示的低级设计,稍后我们还会看到一个实际的示例

停车系统

type ParkingSystem struct {
	issuedTickets map[string]ParkingTicket
	parkingFloor  []*ParkingFloor
	isFull        map[ParkingSpotType]bool
	entryGates    []Gate
	exitGates     []Gate
}

func (this *ParkingSystem) addParkingSpot(parkingSpot ParkingSpot) {}

func (this *ParkingSystem) bookParkingSpot(pSpotType ParkingSpotType) (ParkingSpot, error) {}

func (this *ParkingSystem) issueTicket(pSpotType ParkingSpotType, vehicle Vehicle, entryGate Gate) (*ParkingTicket, error) {}

func (this *ParkingSystem) exitVehicle(ticket *ParkingTicket, pGType PaymentGatewayType) {}

停车楼层

type ParkingFloor struct {
	parkingSpots    map[ParkingSpotType]*ParkingSpotDLL
	parkingSpotsMap map[string]*ParkingSpotDLLNode
	isFull          map[ParkingSpotType]bool
	floor           int
}

func (this *ParkingFloor) addParkingSpot(pSpot ParkingSpot) {}

func (this *ParkingFloor) bookParkingSpot(pSpotType ParkingSpotType) (ParkingSpot, error) {}

func (this *ParkingFloor) freeParkingSpot(pSpot ParkingSpot) {}

停车票

type ParkingTicket struct {
	vehicle     Vehicle
	parkingSpot ParkingSpot
	status      ParkingTicketStatus
	entryTime   time.Time
	exitTime    time.Time
	entryGate   Gate
	exitGate    Gate
	price       int
	pgType      PaymentGatewayType
}

func (this *ParkingTicket) exit(pgType PaymentGatewayType) {}

停车票状态枚举

type ParkingTicketStatus uint8
const (
    active ParkingTicketStatus = iota
    paid
)

停车位接口

type ParkingSpot interface {
	isFull() bool
	getFloor() int
	getLocation() string
	getParkingSpotType() ParkingSpotType
	markFull()
	markFree()
}

停车位类型枚举

type ParkingSpotType uint8
const (
    carPT ParkingSpotType = iota
    truckPT
    motorcyclePT
)

汽车停车位

type CarParkingSpot struct {
	full     bool
	floor    int
	location string
}

func (this *CarParkingSpot) isFull() bool {}

func (this *CarParkingSpot) getFloor() int {}

func (this *CarParkingSpot) getLocation() string {}

func (this *CarParkingSpot) getParkingSpotType() ParkingSpotType {}

func (this *CarParkingSpot) markFull() {}

func (this *CarParkingSpot) markFree() {}

卡车停车位

type TruckParkingSpot struct {
	full     bool
	floor    int
	location string
}

func (this *TruckParkingSpot) isFull() bool {}

func (this *TruckParkingSpot) getFloor() int {}

func (this *TruckParkingSpot) getLocation() string {}

func (this *TruckParkingSpot) getParkingSpotType() ParkingSpotType {}

func (this *TruckParkingSpot) markFull() {}

func (this *TruckParkingSpot) markFree() {}

摩托车停车位

type MotorCycleParkingSpot struct {
	full     bool
	floor    int
	location string
}

func (this *MotorCycleParkingSpot) isFull() bool {}

func (this *MotorCycleParkingSpot) getFloor() int {}

func (this *MotorCycleParkingSpot) getLocation() string {}

func (this *MotorCycleParkingSpot) getParkingSpotType() ParkingSpotType {}

func (this *MotorCycleParkingSpot) markFull() {}

func (this *MotorCycleParkingSpot) markFree() {}

停车费工厂与停车费及子停车费

type ParkingRateFactory struct {
	parkingRateMap map[VehicleType]ParkingRate
}

func (this *ParkingRateFactory) getParkingRateInstanceByVehicleType(vType VehicleType) ParkingRate {}

type ParkingRateBase struct {
	perHourCharges int
}

type ParkingRate interface {
	amountToPay(int) int
}

type CarParkingRate struct {
	ParkingRateBase
}

func (this *CarParkingRate) amountToPay(hours int) int {}

type TruckParkingRate struct {
	ParkingRateBase
}

func (this *TruckParkingRate) amountToPay(hours int) int {}

type MotorCycleParkingRate struct {
	ParkingRateBase
}

func (this *MotorCycleParkingRate) amountToPay(hours int) int {}

支付网关类与子支付网关类

type PaymentGatewayType uint8

const (
	cash PaymentGatewayType = iota
	creditCard
	debitCard
)

type PaymentGatewayFactory struct {
	paymentGatewayMap map[PaymentGatewayType]PaymentGateway
}

func (this *PaymentGatewayFactory) getPaymentGatewayInstanceByPGType(pgType PaymentGatewayType) PaymentGateway {}

type PaymentGateway interface {
	pay(int)
}

type CashPaymentGateway struct {
}

func (this CashPaymentGateway) pay(price int) {}

type CreditCardPaymentGateway struct {
}

func (this CreditCardPaymentGateway) pay(price int) {}

type DebitCardPaymentGateway struct {
}

func (this DebitCardPaymentGateway) pay(price int) {}

程序

下面是完整的工作代码,如果有人对 Go 编程语言感兴趣的话。在下面的示例中,我们将查看两个示例

  • 第一个是一个小型停车场,只有一层和两个汽车停车位

  • 另一个是一个大停车场,拥有两层,每层都有两个汽车停车位、两个摩托车停车位和一个卡车停车位。

我们使用了双向链表来存储停车位列表,这样

  • 我们可以在 O(1)时间内找到一个空闲停车位

  • 我们应该能够在 O(1)时间内重新获取一个停车位

parkingSystem.go

package main

import "fmt"

type ParkingSystem struct {
	issuedTickets map[string]ParkingTicket
	parkingFloor  []*ParkingFloor
	isFull        map[ParkingSpotType]bool
	entryGates    []Gate
	exitGates     []Gate
}

func (this *ParkingSystem) addParkingSpot(parkingSpot ParkingSpot) {
	this.parkingFloor[parkingSpot.getFloor()-1].addParkingSpot(parkingSpot)
}

func (this *ParkingSystem) bookParkingSpot(pSpotType ParkingSpotType) (ParkingSpot, error) {
	for _, pFloor := range this.parkingFloor {
		pSpot, err := pFloor.bookParkingSpot(pSpotType)
		if err == nil {
			return pSpot, nil
		}
	}

	return nil, fmt.Errorf("Cannot issue ticket. All %s parking spot type are full\n", pSpotType.toString())
}

func (this *ParkingSystem) issueTicket(pSpotType ParkingSpotType, vehicle Vehicle, entryGate Gate) (*ParkingTicket, error) {
	fmt.Printf("\nGoing to issue ticket for vehicle number %s\n", vehicle.numberPlate)
	pSpot, err := this.bookParkingSpot(pSpotType)
	if err != nil {
		return nil, err
	}

	ticket := initParkingTicket(vehicle, pSpot, entryGate)
	return ticket, nil
}

func (this *ParkingSystem) printStatus() {
	fmt.Println("\nPrinting Status of Parking Spot")
	for _, pFloor := range this.parkingFloor {
		pFloor.printStatus()
	}
}

func (this *ParkingSystem) exitVehicle(ticket *ParkingTicket, pGType PaymentGatewayType) {
	this.parkingFloor[ticket.parkingSpot.getFloor()-1].freeParkingSpot(ticket.parkingSpot)
	ticket.exit(pGType)
}

parkingFloor.go

package main

import "fmt"

type ParkingFloor struct {
	parkingSpots    map[ParkingSpotType]*ParkingSpotDLL
	parkingSpotsMap map[string]*ParkingSpotDLLNode
	isFull          map[ParkingSpotType]bool
	floor           int
}

func initParkingFloor(floor int) *ParkingFloor {
	return &ParkingFloor{
		floor:           floor,
		parkingSpots:    make(map[ParkingSpotType]*ParkingSpotDLL),
		parkingSpotsMap: make(map[string]*ParkingSpotDLLNode),
		isFull:          make(map[ParkingSpotType]bool),
	}
}

func (this *ParkingFloor) addParkingSpot(pSpot ParkingSpot) {
	dll, ok := this.parkingSpots[pSpot.getParkingSpotType()]
	if ok {
		newNode := &ParkingSpotDLLNode{
			pSpot: pSpot,
		}
		dll.AddToFront(newNode)
		this.parkingSpotsMap[pSpot.getLocation()] = newNode
		return
	}

	dll = &ParkingSpotDLL{}
	this.parkingSpots[pSpot.getParkingSpotType()] = dll
	newNode := &ParkingSpotDLLNode{
		pSpot: pSpot,
	}
	this.parkingSpots[pSpot.getParkingSpotType()].AddToFront(newNode)
	this.parkingSpotsMap[pSpot.getLocation()] = newNode
}

func (this *ParkingFloor) bookParkingSpot(pSpotType ParkingSpotType) (ParkingSpot, error) {
	if this.isFull[pSpotType] {
		return nil, fmt.Errorf("%s Parking Spot is full on %d floor", pSpotType.toString(), this.floor)
	}

	nextPSpot := this.parkingSpots[pSpotType].Front()
	nextPSpot.pSpot.markFull()
	this.parkingSpots[pSpotType].MoveNodeToEnd(nextPSpot)
	if this.parkingSpots[pSpotType].Front().pSpot.isFull() {
		this.isFull[pSpotType] = true
	}
	return nextPSpot.pSpot, nil
}

func (this *ParkingFloor) printStatus() {
	for pSpotType, dll := range this.parkingSpots {
		fmt.Printf("Details of parking spots of type %s on floor %d\n", pSpotType.toString(), this.floor)
		dll.TraverseForward()
	}
}

func (this *ParkingFloor) freeParkingSpot(pSpot ParkingSpot) {
	node := this.parkingSpotsMap[pSpot.getLocation()]
	node.pSpot.markFree()
	this.isFull[pSpot.getParkingSpotType()] = false
	this.parkingSpots[pSpot.getParkingSpotType()].MoveNodeToFront(node)
}

parkingSpot.go

package main

type ParkingSpot interface {
	isFull() bool
	getFloor() int
	getLocation() string
	getParkingSpotType() ParkingSpotType
	markFull()
	markFree()
}

parkingSpotType.go

package main

type ParkingSpotType uint8

const (
	carPT ParkingSpotType = iota
	truckPT
	motorcyclePT
)

func (s ParkingSpotType) toString() string {
	switch s {
	case carPT:
		return "Car Parking Type"
	case truckPT:
		return "Truck Parking Type"
	case motorcyclePT:
		return "Motorcylce Parking Type"
	}
	return ""
}

func initParkingSpot(floor int, partkingSpotType ParkingSpotType, location string) ParkingSpot {
	switch partkingSpotType {
	case carPT:
		return &CarParkingSpot{full: false,
			floor:    floor,
			location: location,
		}
	case truckPT:
		return &TruckParkingSpot{full: false,
			floor:    floor,
			location: location,
		}
	case motorcyclePT:
		return &MotorCycleParkingSpot{full: false,
			floor:    floor,
			location: location,
		}
	}
	return nil
}

carParkingSpot.go

package main

type CarParkingSpot struct {
	full     bool
	floor    int
	location string
}

func (this *CarParkingSpot) isFull() bool {
	return this.full
}

func (this *CarParkingSpot) getFloor() int {
	return this.floor
}

func (this *CarParkingSpot) getLocation() string {
	return this.location
}

func (this *CarParkingSpot) getParkingSpotType() ParkingSpotType {
	return carPT
}

func (this *CarParkingSpot) markFull() {
	this.full = true
}

func (this *CarParkingSpot) markFree() {
	this.full = true
}

truckParkingSpot.go

package main

type TruckParkingSpot struct {
	full     bool
	floor    int
	location string
}

func (this *TruckParkingSpot) isFull() bool {
	return this.full
}

func (this *TruckParkingSpot) getFloor() int {
	return this.floor
}

func (this *TruckParkingSpot) getLocation() string {
	return this.location
}

func (this *TruckParkingSpot) getParkingSpotType() ParkingSpotType {
	return truckPT
}

func (this *TruckParkingSpot) markFull() {
	this.full = true
}

func (this *TruckParkingSpot) markFree() {
	this.full = true
}

motorcycleParkingSpot.go

package main

type MotorCycleParkingSpot struct {
	full     bool
	floor    int
	location string
}

func (this *MotorCycleParkingSpot) isFull() bool {
	return this.full
}

func (this *MotorCycleParkingSpot) getFloor() int {
	return this.floor
}

func (this *MotorCycleParkingSpot) getLocation() string {
	return this.location
}

func (this *MotorCycleParkingSpot) getParkingSpotType() ParkingSpotType {
	return motorcyclePT
}

func (this *MotorCycleParkingSpot) markFull() {
	this.full = true
}

func (this *MotorCycleParkingSpot) markFree() {
	this.full = true
}

parkingTicket.go

package main

import (
	"fmt"
	"time"
)

type ParkingTicket struct {
	vehicle     Vehicle
	parkingSpot ParkingSpot
	status      ParkingTicketStatus
	entryTime   time.Time
	exitTime    time.Time
	entryGate   Gate
	exitGate    Gate
	price       int
	pgType      PaymentGatewayType
}

func initParkingTicket(vehicle Vehicle, pSpot ParkingSpot, entryGate Gate) *ParkingTicket {
	return &ParkingTicket{
		vehicle:     vehicle,
		parkingSpot: pSpot,
		status:      active,
		entryTime:   time.Now(),
		entryGate:   entryGate,
	}
}

func (this *ParkingTicket) exit(pgType PaymentGatewayType) {
	fmt.Printf("Vehicle with number %s exiting from Parking Lot\n", this.vehicle.numberPlate)
	this.exitTime = time.Now()
	pRateInstance := pRateFactorySingleInstance.getParkingRateInstanceByVehicleType(this.vehicle.vehicleType)
	totalDurationInHours := int(this.exitTime.Sub(this.entryTime).Hours())
	this.price = pRateInstance.amountToPay(totalDurationInHours) + 1
	this.pgType = pgType
	pgInstance := pgFactorySingleInstance.getPaymentGatewayInstanceByPGType(pgType)
	pgInstance.pay(this.price)
	this.status = paid
}

func (this *ParkingTicket) print() {
	fmt.Printf("Issued ticket for vehicle number %s at parking spot %s\n ", this.vehicle.numberPlate, this.parkingSpot.getLocation())
	//fmt.Printf("\nPrinting Ticket\n")
	//fmt.Printf("Status: %s, \nEntryTime: %s, \nEntryGate: %d, \nVehicle: %s, \nParking Spot: \n\n", this.status.toString(), this.entryTime.String(), this.entryGate, this.vehicle.toString())
}

parkingTicketStatus.go

package main

type ParkingTicketStatus uint8

const (
	active ParkingTicketStatus = iota
	paid
)

func (s ParkingTicketStatus) toString() string {
	switch s {
	case active:
		return "Active"
	case paid:
		return "Paid"
	}
	return ""
}

dll.go

package main

import "fmt"

type ParkingSpotDLLNode struct {
	pSpot ParkingSpot
	prev  *ParkingSpotDLLNode
	next  *ParkingSpotDLLNode
}

type ParkingSpotDLL struct {
	len  int
	tail *ParkingSpotDLLNode
	head *ParkingSpotDLLNode
}

func initDoublyList() *ParkingSpotDLL {
	return &ParkingSpotDLL{}
}

func (d *ParkingSpotDLL) AddToFront(node *ParkingSpotDLLNode) {

	if d.head == nil {
		d.head = node
		d.tail = node
	} else {
		node.next = d.head
		d.head.prev = node
		d.head = node
	}
	d.len++
	return
}

func (d *ParkingSpotDLL) RemoveFromFront() {
	if d.head == nil {
		return
	} else if d.head == d.tail {
		d.head = nil
		d.tail = nil
	} else {
		d.head = d.head.next
	}
	d.len--
}

func (d *ParkingSpotDLL) AddToEnd(node *ParkingSpotDLLNode) {
	newNode := node
	if d.head == nil {
		d.head = newNode
		d.tail = newNode
	} else {
		currentNode := d.head
		for currentNode.next != nil {
			currentNode = currentNode.next
		}
		newNode.prev = currentNode
		currentNode.next = newNode
		d.tail = newNode
	}
	d.len++
}
func (d *ParkingSpotDLL) Front() *ParkingSpotDLLNode {
	return d.head
}

func (d *ParkingSpotDLL) MoveNodeToEnd(node *ParkingSpotDLLNode) {
	prev := node.prev
	next := node.next

	if prev != nil {
		prev.next = next
	}

	if next != nil {
		next.prev = prev
	}
	if d.tail == node {
		d.tail = prev
	}
	if d.head == node {
		d.head = next
	}
	node.next = nil
	node.prev = nil
	d.len--
	d.AddToEnd(node)
}

func (d *ParkingSpotDLL) MoveNodeToFront(node *ParkingSpotDLLNode) {
	prev := node.prev
	next := node.next

	if prev != nil {
		prev.next = next
	}

	if next != nil {
		next.prev = prev
	}
	if d.tail == node {
		d.tail = prev
	}
	if d.head == node {
		d.head = next
	}
	node.next = nil
	node.prev = nil
	d.len--
	d.AddToFront(node)
}

func (d *ParkingSpotDLL) TraverseForward() error {
	if d.head == nil {
		return fmt.Errorf("TraverseError: List is empty")
	}
	temp := d.head
	for temp != nil {
		fmt.Printf("Location = %v, parkingType = %s, floor = %d full =%t\n", temp.pSpot.getLocation(), temp.pSpot.getParkingSpotType().toString(), temp.pSpot.getFloor(), temp.pSpot.isFull())
		temp = temp.next
	}
	fmt.Println()
	return nil
}

func (d *ParkingSpotDLL) Size() int {
	return d.len
}

gate.go

package main

type Gate struct {
	floor    int
	gateType GateType
}

func initGate(floor int, gateType GateType) Gate {
	return Gate{
		floor:    floor,
		gateType: gateType,
	}
}

gateType.go

package main

type GateType uint8

const (
	entryGateType GateType = iota
	exitGateType  GateType = iota
)

vehicle.go

package main

import "fmt"

type Vehicle struct {
	numberPlate string
	vehicleType VehicleType
}

func (v Vehicle) toString() string {
	return fmt.Sprintf("{NumberPlate: %s, VehicleType: %s}", v.numberPlate, v.vehicleType.toString())
}

vehicleType.go

package main

type VehicleType uint8

const (
	car VehicleType = iota
	truck
	motorcycle
)

func (s VehicleType) toString() string {
	switch s {
	case car:
		return "Car"
	case truck:
		return "Truck"
	case motorcycle:
		return "Motorcylce"
	}
	return ""
}

parkingRate.go

package main

type ParkingRateFactory struct {
	parkingRateMap map[VehicleType]ParkingRate
}

func (this *ParkingRateFactory) getParkingRateInstanceByVehicleType(vType VehicleType) ParkingRate {
	if this.parkingRateMap[vType] != nil {
		return this.parkingRateMap[vType]
	}
	if vType == car {
		this.parkingRateMap[vType] = &CarParkingRate{
			ParkingRateBase{perHourCharges: 2},
		}
		return this.parkingRateMap[vType]
	}
	if vType == truck {
		this.parkingRateMap[vType] = &TruckParkingRate{
			ParkingRateBase{perHourCharges: 3},
		}
		return this.parkingRateMap[vType]
	}
	if vType == motorcycle {
		this.parkingRateMap[vType] = &MotorCycleParkingRate{
			ParkingRateBase{perHourCharges: 1},
		}
		return this.parkingRateMap[vType]
	}
	return nil
}

type ParkingRateBase struct {
	perHourCharges int
}

type ParkingRate interface {
	amountToPay(int) int
}

type CarParkingRate struct {
	ParkingRateBase
}

func (this *CarParkingRate) amountToPay(hours int) int {
	return this.perHourCharges * hours
}

type TruckParkingRate struct {
	ParkingRateBase
}

func (this *TruckParkingRate) amountToPay(hours int) int {
	return this.perHourCharges * hours
}

type MotorCycleParkingRate struct {
	ParkingRateBase
}

func (this *MotorCycleParkingRate) amountToPay(hours int) int {
	return this.perHourCharges * hours
}

paymentGateway.go

package main

import "fmt"

type PaymentGatewayType uint8

const (
	cash PaymentGatewayType = iota
	creditCard
	debitCard
)

type PaymentGatewayFactory struct {
	paymentGatewayMap map[PaymentGatewayType]PaymentGateway
}

func (this *PaymentGatewayFactory) getPaymentGatewayInstanceByPGType(pgType PaymentGatewayType) PaymentGateway {
	if this.paymentGatewayMap[pgType] != nil {
		return this.paymentGatewayMap[pgType]
	}
	if pgType == cash {
		this.paymentGatewayMap[pgType] = &CashPaymentGateway{}
		return this.paymentGatewayMap[pgType]
	}
	if pgType == creditCard {
		this.paymentGatewayMap[pgType] = &CreditCardPaymentGateway{}
		return this.paymentGatewayMap[pgType]
	}
	if pgType == debitCard {
		this.paymentGatewayMap[pgType] = &DebitCardPaymentGateway{}
		return this.paymentGatewayMap[pgType]
	}
	return nil
}

type PaymentGateway interface {
	pay(int)
}

type CashPaymentGateway struct {
}

func (this CashPaymentGateway) pay(price int) {
	fmt.Printf("Paying price of %d$ through cash payment\n", price)
}

type CreditCardPaymentGateway struct {
}

func (this CreditCardPaymentGateway) pay(price int) {
	fmt.Printf("Paying price of %d$ through credit card payment\n", price)
}

type DebitCardPaymentGateway struct {
}

func (this DebitCardPaymentGateway) pay(price int) {
	fmt.Printf("Paying price of %d$ through debit card payment\n", price)
}

main.go

package main

import (
	"fmt"
)

var (
	pRateFactorySingleInstance = &ParkingRateFactory{
		parkingRateMap: make(map[VehicleType]ParkingRate),
	}
	pgFactorySingleInstance = &PaymentGatewayFactory{
		paymentGatewayMap: make(map[PaymentGatewayType]PaymentGateway),
	}
)

func main() {

	testSmallParkingLot()

	testLargeParkingLot()

}

func testSmallParkingLot() {
	firstParkingFloor := initParkingFloor(1)
	firstFloorEntryGate1 := initGate(1, entryGateType)
	firstFloorExitGate := initGate(1, exitGateType)
	firstFloorCarParkingSpot1 := initParkingSpot(1, carPT, "A1")
	firstFloorCarParkingSpot2 := initParkingSpot(1, carPT, "A2")

	parkingSystem := ParkingSystem{
		parkingFloor:  []*ParkingFloor{firstParkingFloor},
		entryGates:    []Gate{firstFloorEntryGate1},
		exitGates:     []Gate{firstFloorExitGate},
		issuedTickets: make(map[string]ParkingTicket),
	}
	//Add first floor parkings
	parkingSystem.addParkingSpot(firstFloorCarParkingSpot1)
	parkingSystem.addParkingSpot(firstFloorCarParkingSpot2)

	carVehicle1 := Vehicle{
		numberPlate: "C1",
		vehicleType: car,
	}
	carVehicle2 := Vehicle{
		numberPlate: "C2",
		vehicleType: car,
	}

	parkingSystem.printStatus()
	carVehicleTicket1, err := parkingSystem.issueTicket(carPT, carVehicle1, firstFloorEntryGate1)
	if err != nil {
		fmt.Println(err)
	}
	carVehicleTicket1.print()

	carVehicleTicket2, err := parkingSystem.issueTicket(carPT, carVehicle2, firstFloorEntryGate1)
	if err != nil {
		fmt.Println(err)
	}
	carVehicleTicket2.print()

	carVehicle3 := Vehicle{
		numberPlate: "C3",
		vehicleType: car,
	}
	carVehicleTicket3, err := parkingSystem.issueTicket(carPT, carVehicle3, firstFloorEntryGate1)
	if err != nil {
		fmt.Println(err)
	}
	parkingSystem.printStatus()

	parkingSystem.exitVehicle(carVehicleTicket1, cash)
	parkingSystem.printStatus()

	carVehicleTicket3, err = parkingSystem.issueTicket(carPT, carVehicle3, firstFloorEntryGate1)
	if err != nil {
		fmt.Println(err)
	}
	carVehicleTicket3.print()
	parkingSystem.printStatus()

}

func testLargeParkingLot() {
	//We have two parking floor
	firstParkingFloor := initParkingFloor(1)
	secondParkingFloor := initParkingFloor(2)

	//We have two entry gates in firstParkingFloor
	firstFloorEntryGate1 := initGate(1, entryGateType)
	firstFloorEntryGate2 := initGate(1, entryGateType)

	//We have one exit gate on firstParkingFloor
	firstFloorExitGate := initGate(1, exitGateType)

	parkingSystem := ParkingSystem{
		parkingFloor:  []*ParkingFloor{firstParkingFloor, secondParkingFloor},
		entryGates:    []Gate{firstFloorEntryGate1, firstFloorEntryGate2},
		exitGates:     []Gate{firstFloorExitGate},
		issuedTickets: make(map[string]ParkingTicket),
	}

	//We have two car parking spots, two motorcyle parking spots, 1 truck paring spot on each of the floor
	firstFloorCarParkingSpot1 := initParkingSpot(1, carPT, "A1")
	firstFloorCarParkingSpot2 := initParkingSpot(1, carPT, "A2")
	firstFloorMotorCycleParkingSpot1 := initParkingSpot(1, motorcyclePT, "A3")
	firstFloorMotorCycleParkingSpot2 := initParkingSpot(1, motorcyclePT, "A4")
	firstFloorTruckParkingSpot := initParkingSpot(1, truckPT, "A5")

	//We have two car parking spots, two motorcyle parking spots, 1 truck paring spot on each of the floor
	secondFloorCarParkingSpot1 := initParkingSpot(2, carPT, "B1")
	secondFloorCarParkingSpot2 := initParkingSpot(2, carPT, "B2")
	secondFloorMotorCycleParkingSpot1 := initParkingSpot(2, motorcyclePT, "B3")
	secondFloorMotorCycleParkingSpot2 := initParkingSpot(2, motorcyclePT, "B4")
	secondFloorTruckParkingSpot := initParkingSpot(2, truckPT, "B5")

	//Add first floor parkings
	parkingSystem.addParkingSpot(firstFloorCarParkingSpot1)
	parkingSystem.addParkingSpot(firstFloorCarParkingSpot2)
	parkingSystem.addParkingSpot(firstFloorMotorCycleParkingSpot1)
	parkingSystem.addParkingSpot(firstFloorMotorCycleParkingSpot2)
	parkingSystem.addParkingSpot(firstFloorTruckParkingSpot)

	//Add second floor parkings
	parkingSystem.addParkingSpot(secondFloorCarParkingSpot1)
	parkingSystem.addParkingSpot(secondFloorCarParkingSpot2)
	parkingSystem.addParkingSpot(secondFloorMotorCycleParkingSpot1)
	parkingSystem.addParkingSpot(secondFloorMotorCycleParkingSpot2)
	parkingSystem.addParkingSpot(secondFloorTruckParkingSpot)

	carVehicle1 := Vehicle{
		numberPlate: "C1",
		vehicleType: car,
	}
	carVehicle2 := Vehicle{
		numberPlate: "C2",
		vehicleType: car,
	}
	motorCycleVehicle1 := Vehicle{
		numberPlate: "M1",
		vehicleType: motorcycle,
	}
	motorCycleVehicle2 := Vehicle{
		numberPlate: "M2",
		vehicleType: motorcycle,
	}

	truckVehicle1 := Vehicle{
		numberPlate: "T1",
		vehicleType: motorcycle,
	}

	parkingSystem.printStatus()
	carVehicleTicket1, err := parkingSystem.issueTicket(carPT, carVehicle1, firstFloorEntryGate1)
	if err != nil {
		fmt.Println(err)
	}
	carVehicleTicket1.print()
	carVehicleTicket2, err := parkingSystem.issueTicket(carPT, carVehicle2, firstFloorEntryGate1)
	if err != nil {
		fmt.Println(err)
	}
	carVehicleTicket2.print()
	motorCycleVehicleTicket1, err := parkingSystem.issueTicket(motorcyclePT, motorCycleVehicle1, firstFloorEntryGate1)
	if err != nil {
		fmt.Println(err)
	}
	motorCycleVehicleTicket1.print()
	motorCycleVehicleTicket2, err := parkingSystem.issueTicket(motorcyclePT, motorCycleVehicle2, firstFloorEntryGate1)
	if err != nil {
		fmt.Println(err)
	}
	motorCycleVehicleTicket2.print()
	truckVehicleTicket1, err := parkingSystem.issueTicket(truckPT, truckVehicle1, firstFloorEntryGate1)
	if err != nil {
		fmt.Println(err)
	}
	truckVehicleTicket1.print()
	parkingSystem.printStatus()

	carVehicle3 := Vehicle{
		numberPlate: "C3",
		vehicleType: car,
	}
	carVehicle4 := Vehicle{
		numberPlate: "C4",
		vehicleType: car,
	}
	motorCycleVehicle3 := Vehicle{
		numberPlate: "M3",
		vehicleType: motorcycle,
	}
	motorCycleVehicle4 := Vehicle{
		numberPlate: "M4",
		vehicleType: motorcycle,
	}

	truckVehicle2 := Vehicle{
		numberPlate: "T2",
		vehicleType: motorcycle,
	}
	carVehicleTicket3, err := parkingSystem.issueTicket(carPT, carVehicle3, firstFloorEntryGate1)
	if err != nil {
		fmt.Println(err)
	}
	carVehicleTicket3.print()
	carVehicleTicket4, err := parkingSystem.issueTicket(carPT, carVehicle4, firstFloorEntryGate1)
	if err != nil {
		fmt.Println(err)
	}
	carVehicleTicket4.print()
	motorCycleVehicleTicket3, err := parkingSystem.issueTicket(motorcyclePT, motorCycleVehicle3, firstFloorEntryGate1)
	if err != nil {
		fmt.Println(err)
	}
	motorCycleVehicleTicket3.print()
	motorCycleVehicleTicket4, err := parkingSystem.issueTicket(motorcyclePT, motorCycleVehicle4, firstFloorEntryGate1)
	if err != nil {
		fmt.Println(err)
	}
	motorCycleVehicleTicket4.print()
	truckVehicleTicket2, err := parkingSystem.issueTicket(truckPT, truckVehicle2, firstFloorEntryGate1)
	if err != nil {
		fmt.Println(err)
	}
	truckVehicleTicket2.print()
	parkingSystem.printStatus()

	carVehicle5 := Vehicle{
		numberPlate: "C5",
		vehicleType: car,
	}
	carVehicleTicket5, err := parkingSystem.issueTicket(carPT, carVehicle5, firstFloorEntryGate1)
	if err != nil {
		fmt.Println(err)
	}

	parkingSystem.printStatus()

	parkingSystem.exitVehicle(carVehicleTicket1, cash)
	parkingSystem.printStatus()

	carVehicleTicket5, err = parkingSystem.issueTicket(carPT, carVehicle5, firstFloorEntryGate1)
	if err != nil {
		fmt.Println(err)
	}
	carVehicleTicket5.print()
	parkingSystem.printStatus()
}

输出

Printing Status of Parking Spot
Details of parking spots of type Car Parking Type on floor 1
Location = A2, parkingType = Car Parking Type, floor = 1 full =false
Location = A1, parkingType = Car Parking Type, floor = 1 full =false

Going to issue ticket for vehicle number C1
Issued ticket for vehicle number C1 at parking spot A2
Going to issue ticket for vehicle number C2
Issued ticket for vehicle number C2 at parking spot A1
Going to issue ticket for vehicle number C3
Cannot issue ticket. All Car Parking Type parking spot type are full

Printing Status of Parking Spot
Details of parking spots of type Car Parking Type on floor 1
Location = A2, parkingType = Car Parking Type, floor = 1 full =true
Location = A1, parkingType = Car Parking Type, floor = 1 full =true

Vehicle with number C1 exiting from Parking Lot
Paying price of 1$ through cash payment

Printing Status of Parking Spot
Details of parking spots of type Car Parking Type on floor 1
Location = A2, parkingType = Car Parking Type, floor = 1 full =true
Location = A1, parkingType = Car Parking Type, floor = 1 full =true

Going to issue ticket for vehicle number C3
Issued ticket for vehicle number C3 at parking spot A2
Printing Status of Parking Spot
Details of parking spots of type Car Parking Type on floor 1
Location = A1, parkingType = Car Parking Type, floor = 1 full =true
Location = A2, parkingType = Car Parking Type, floor = 1 full =true

单文件完整工作代码

这里是完整的工作代码

package main

import (
	"fmt"
	"time"
)

type CarParkingSpot struct {
	full     bool
	floor    int
	location string
}

func (this *CarParkingSpot) isFull() bool {
	return this.full
}

func (this *CarParkingSpot) getFloor() int {
	return this.floor
}

func (this *CarParkingSpot) getLocation() string {
	return this.location
}

func (this *CarParkingSpot) getParkingSpotType() ParkingSpotType {
	return carPT
}

func (this *CarParkingSpot) markFull() {
	this.full = true
}

func (this *CarParkingSpot) markFree() {
	this.full = true
}

type ParkingSpotDLLNode struct {
	pSpot ParkingSpot
	prev  *ParkingSpotDLLNode
	next  *ParkingSpotDLLNode
}

type ParkingSpotDLL struct {
	len  int
	tail *ParkingSpotDLLNode
	head *ParkingSpotDLLNode
}

func initDoublyList() *ParkingSpotDLL {
	return &ParkingSpotDLL{}
}

func (d *ParkingSpotDLL) AddToFront(node *ParkingSpotDLLNode) {

	if d.head == nil {
		d.head = node
		d.tail = node
	} else {
		node.next = d.head
		d.head.prev = node
		d.head = node
	}
	d.len++
	return
}

func (d *ParkingSpotDLL) RemoveFromFront() {
	if d.head == nil {
		return
	} else if d.head == d.tail {
		d.head = nil
		d.tail = nil
	} else {
		d.head = d.head.next
	}
	d.len--
}

func (d *ParkingSpotDLL) AddToEnd(node *ParkingSpotDLLNode) {
	newNode := node
	if d.head == nil {
		d.head = newNode
		d.tail = newNode
	} else {
		currentNode := d.head
		for currentNode.next != nil {
			currentNode = currentNode.next
		}
		newNode.prev = currentNode
		currentNode.next = newNode
		d.tail = newNode
	}
	d.len++
}
func (d *ParkingSpotDLL) Front() *ParkingSpotDLLNode {
	return d.head
}

func (d *ParkingSpotDLL) MoveNodeToEnd(node *ParkingSpotDLLNode) {
	prev := node.prev
	next := node.next

	if prev != nil {
		prev.next = next
	}

	if next != nil {
		next.prev = prev
	}
	if d.tail == node {
		d.tail = prev
	}
	if d.head == node {
		d.head = next
	}
	node.next = nil
	node.prev = nil
	d.len--
	d.AddToEnd(node)
}

func (d *ParkingSpotDLL) MoveNodeToFront(node *ParkingSpotDLLNode) {
	prev := node.prev
	next := node.next

	if prev != nil {
		prev.next = next
	}

	if next != nil {
		next.prev = prev
	}
	if d.tail == node {
		d.tail = prev
	}
	if d.head == node {
		d.head = next
	}
	node.next = nil
	node.prev = nil
	d.len--
	d.AddToFront(node)
}

func (d *ParkingSpotDLL) TraverseForward() error {
	if d.head == nil {
		return fmt.Errorf("TraverseError: List is empty")
	}
	temp := d.head
	for temp != nil {
		fmt.Printf("Location = %v, parkingType = %s, floor = %d full =%t\n", temp.pSpot.getLocation(), temp.pSpot.getParkingSpotType().toString(), temp.pSpot.getFloor(), temp.pSpot.isFull())
		temp = temp.next
	}
	fmt.Println()
	return nil
}

func (d *ParkingSpotDLL) Size() int {
	return d.len
}

type Gate struct {
	floor    int
	gateType GateType
}

func initGate(floor int, gateType GateType) Gate {
	return Gate{
		floor:    floor,
		gateType: gateType,
	}
}

type GateType uint8

const (
	entryGateType GateType = iota
	exitGateType  GateType = iota
)

var (
	pRateFactorySingleInstance = &ParkingRateFactory{
		parkingRateMap: make(map[VehicleType]ParkingRate),
	}
	pgFactorySingleInstance = &PaymentGatewayFactory{
		paymentGatewayMap: make(map[PaymentGatewayType]PaymentGateway),
	}
)

type MotorCycleParkingSpot struct {
	full     bool
	floor    int
	location string
}

func (this *MotorCycleParkingSpot) isFull() bool {
	return this.full
}

func (this *MotorCycleParkingSpot) getFloor() int {
	return this.floor
}

func (this *MotorCycleParkingSpot) getLocation() string {
	return this.location
}

func (this *MotorCycleParkingSpot) getParkingSpotType() ParkingSpotType {
	return motorcyclePT
}

func (this *MotorCycleParkingSpot) markFull() {
	this.full = true
}

func (this *MotorCycleParkingSpot) markFree() {
	this.full = true
}

type ParkingFloor struct {
	parkingSpots    map[ParkingSpotType]*ParkingSpotDLL
	parkingSpotsMap map[string]*ParkingSpotDLLNode
	isFull          map[ParkingSpotType]bool
	floor           int
}

func initParkingFloor(floor int) *ParkingFloor {
	return &ParkingFloor{
		floor:           floor,
		parkingSpots:    make(map[ParkingSpotType]*ParkingSpotDLL),
		parkingSpotsMap: make(map[string]*ParkingSpotDLLNode),
		isFull:          make(map[ParkingSpotType]bool),
	}
}

func (this *ParkingFloor) addParkingSpot(pSpot ParkingSpot) {
	dll, ok := this.parkingSpots[pSpot.getParkingSpotType()]
	if ok {
		newNode := &ParkingSpotDLLNode{
			pSpot: pSpot,
		}
		dll.AddToFront(newNode)
		this.parkingSpotsMap[pSpot.getLocation()] = newNode
		return
	}

	dll = &ParkingSpotDLL{}
	this.parkingSpots[pSpot.getParkingSpotType()] = dll
	newNode := &ParkingSpotDLLNode{
		pSpot: pSpot,
	}
	this.parkingSpots[pSpot.getParkingSpotType()].AddToFront(newNode)
	this.parkingSpotsMap[pSpot.getLocation()] = newNode
}

func (this *ParkingFloor) bookParkingSpot(pSpotType ParkingSpotType) (ParkingSpot, error) {
	if this.isFull[pSpotType] {
		return nil, fmt.Errorf("%s Parking Spot is full on %d floor", pSpotType.toString(), this.floor)
	}

	nextPSpot := this.parkingSpots[pSpotType].Front()
	nextPSpot.pSpot.markFull()
	this.parkingSpots[pSpotType].MoveNodeToEnd(nextPSpot)
	if this.parkingSpots[pSpotType].Front().pSpot.isFull() {
		this.isFull[pSpotType] = true
	}
	return nextPSpot.pSpot, nil
}

func (this *ParkingFloor) printStatus() {
	for pSpotType, dll := range this.parkingSpots {
		fmt.Printf("Details of parking spots of type %s on floor %d\n", pSpotType.toString(), this.floor)
		dll.TraverseForward()
	}
}

func (this *ParkingFloor) freeParkingSpot(pSpot ParkingSpot) {
	node := this.parkingSpotsMap[pSpot.getLocation()]
	node.pSpot.markFree()
	this.isFull[pSpot.getParkingSpotType()] = false
	this.parkingSpots[pSpot.getParkingSpotType()].MoveNodeToFront(node)
}

type ParkingRateFactory struct {
	parkingRateMap map[VehicleType]ParkingRate
}

func (this *ParkingRateFactory) getParkingRateInstanceByVehicleType(vType VehicleType) ParkingRate {
	if this.parkingRateMap[vType] != nil {
		return this.parkingRateMap[vType]
	}
	if vType == car {
		this.parkingRateMap[vType] = &CarParkingRate{
			ParkingRateBase{perHourCharges: 2},
		}
		return this.parkingRateMap[vType]
	}
	if vType == truck {
		this.parkingRateMap[vType] = &TruckParkingRate{
			ParkingRateBase{perHourCharges: 3},
		}
		return this.parkingRateMap[vType]
	}
	if vType == motorcycle {
		this.parkingRateMap[vType] = &MotorCycleParkingRate{
			ParkingRateBase{perHourCharges: 1},
		}
		return this.parkingRateMap[vType]
	}
	return nil
}

type ParkingRateBase struct {
	perHourCharges int
}

type ParkingRate interface {
	amountToPay(int) int
}

type CarParkingRate struct {
	ParkingRateBase
}

func (this *CarParkingRate) amountToPay(hours int) int {
	return this.perHourCharges * hours
}

type TruckParkingRate struct {
	ParkingRateBase
}

func (this *TruckParkingRate) amountToPay(hours int) int {
	return this.perHourCharges * hours
}

type MotorCycleParkingRate struct {
	ParkingRateBase
}

func (this *MotorCycleParkingRate) amountToPay(hours int) int {
	return this.perHourCharges * hours
}

type ParkingSpot interface {
	isFull() bool
	getFloor() int
	getLocation() string
	getParkingSpotType() ParkingSpotType
	markFull()
	markFree()
}

type ParkingSpotType uint8

const (
	carPT ParkingSpotType = iota
	truckPT
	motorcyclePT
)

func (s ParkingSpotType) toString() string {
	switch s {
	case carPT:
		return "Car Parking Type"
	case truckPT:
		return "Truck Parking Type"
	case motorcyclePT:
		return "Motorcylce Parking Type"
	}
	return ""
}

func initParkingSpot(floor int, partkingSpotType ParkingSpotType, location string) ParkingSpot {
	switch partkingSpotType {
	case carPT:
		return &CarParkingSpot{full: false,
			floor:    floor,
			location: location,
		}
	case truckPT:
		return &TruckParkingSpot{full: false,
			floor:    floor,
			location: location,
		}
	case motorcyclePT:
		return &MotorCycleParkingSpot{full: false,
			floor:    floor,
			location: location,
		}
	}
	return nil
}

type ParkingSystem struct {
	issuedTickets map[string]ParkingTicket
	parkingFloor  []*ParkingFloor
	isFull        map[ParkingSpotType]bool
	entryGates    []Gate
	exitGates     []Gate
}

func (this *ParkingSystem) addParkingSpot(parkingSpot ParkingSpot) {
	this.parkingFloor[parkingSpot.getFloor()-1].addParkingSpot(parkingSpot)
}

func (this *ParkingSystem) bookParkingSpot(pSpotType ParkingSpotType) (ParkingSpot, error) {
	for _, pFloor := range this.parkingFloor {
		pSpot, err := pFloor.bookParkingSpot(pSpotType)
		if err == nil {
			return pSpot, nil
		}
	}

	return nil, fmt.Errorf("Cannot issue ticket. All %s parking spot type are full\n", pSpotType.toString())
}

func (this *ParkingSystem) issueTicket(pSpotType ParkingSpotType, vehicle Vehicle, entryGate Gate) (*ParkingTicket, error) {
	fmt.Printf("\nGoing to issue ticket for vehicle number %s\n", vehicle.numberPlate)
	pSpot, err := this.bookParkingSpot(pSpotType)
	if err != nil {
		return nil, err
	}

	ticket := initParkingTicket(vehicle, pSpot, entryGate)
	return ticket, nil
}

func (this *ParkingSystem) printStatus() {
	fmt.Println("\nPrinting Status of Parking Spot")
	for _, pFloor := range this.parkingFloor {
		pFloor.printStatus()
	}
}

func (this *ParkingSystem) exitVehicle(ticket *ParkingTicket, pGType PaymentGatewayType) {
	this.parkingFloor[ticket.parkingSpot.getFloor()-1].freeParkingSpot(ticket.parkingSpot)
	ticket.exit(pGType)
}

type ParkingTicket struct {
	vehicle     Vehicle
	parkingSpot ParkingSpot
	status      ParkingTicketStatus
	entryTime   time.Time
	exitTime    time.Time
	entryGate   Gate
	exitGate    Gate
	price       int
	pgType      PaymentGatewayType
}

func initParkingTicket(vehicle Vehicle, pSpot ParkingSpot, entryGate Gate) *ParkingTicket {
	return &ParkingTicket{
		vehicle:     vehicle,
		parkingSpot: pSpot,
		status:      active,
		entryTime:   time.Now(),
		entryGate:   entryGate,
	}
}

func (this *ParkingTicket) exit(pgType PaymentGatewayType) {
	fmt.Printf("Vehicle with number %s exiting from Parking Lot\n", this.vehicle.numberPlate)
	this.exitTime = time.Now()
	pRateInstance := pRateFactorySingleInstance.getParkingRateInstanceByVehicleType(this.vehicle.vehicleType)
	totalDurationInHours := int(this.exitTime.Sub(this.entryTime).Hours())
	this.price = pRateInstance.amountToPay(totalDurationInHours) + 1
	this.pgType = pgType
	pgInstance := pgFactorySingleInstance.getPaymentGatewayInstanceByPGType(pgType)
	pgInstance.pay(this.price)
	this.status = paid
}

func (this *ParkingTicket) print() {
	fmt.Printf("Issued ticket for vehicle number %s at parking spot %s\n ", this.vehicle.numberPlate, this.parkingSpot.getLocation())
	//fmt.Printf("\nPrinting Ticket\n")
	//fmt.Printf("Status: %s, \nEntryTime: %s, \nEntryGate: %d, \nVehicle: %s, \nParking Spot: \n\n", this.status.toString(), this.entryTime.String(), this.entryGate, this.vehicle.toString())
}

type ParkingTicketStatus uint8

const (
	active ParkingTicketStatus = iota
	paid
)

func (s ParkingTicketStatus) toString() string {
	switch s {
	case active:
		return "Active"
	case paid:
		return "Paid"
	}
	return ""
}

type PaymentGatewayType uint8

const (
	cash PaymentGatewayType = iota
	creditCard
	debitCard
)

type PaymentGatewayFactory struct {
	paymentGatewayMap map[PaymentGatewayType]PaymentGateway
}

func (this *PaymentGatewayFactory) getPaymentGatewayInstanceByPGType(pgType PaymentGatewayType) PaymentGateway {
	if this.paymentGatewayMap[pgType] != nil {
		return this.paymentGatewayMap[pgType]
	}
	if pgType == cash {
		this.paymentGatewayMap[pgType] = &CashPaymentGateway{}
		return this.paymentGatewayMap[pgType]
	}
	if pgType == creditCard {
		this.paymentGatewayMap[pgType] = &CreditCardPaymentGateway{}
		return this.paymentGatewayMap[pgType]
	}
	if pgType == debitCard {
		this.paymentGatewayMap[pgType] = &DebitCardPaymentGateway{}
		return this.paymentGatewayMap[pgType]
	}
	return nil
}

type PaymentGateway interface {
	pay(int)
}

type CashPaymentGateway struct {
}

func (this CashPaymentGateway) pay(price int) {
	fmt.Printf("Paying price of %d$ through cash payment\n", price)
}

type CreditCardPaymentGateway struct {
}

func (this CreditCardPaymentGateway) pay(price int) {
	fmt.Printf("Paying price of %d$ through credit card payment\n", price)
}

type DebitCardPaymentGateway struct {
}

func (this DebitCardPaymentGateway) pay(price int) {
	fmt.Printf("Paying price of %d$ through debit card payment\n", price)
}

type TruckParkingSpot struct {
	full     bool
	floor    int
	location string
}

func (this *TruckParkingSpot) isFull() bool {
	return this.full
}

func (this *TruckParkingSpot) getFloor() int {
	return this.floor
}

func (this *TruckParkingSpot) getLocation() string {
	return this.location
}

func (this *TruckParkingSpot) getParkingSpotType() ParkingSpotType {
	return truckPT
}

func (this *TruckParkingSpot) markFull() {
	this.full = true
}

func (this *TruckParkingSpot) markFree() {
	this.full = true
}

type Vehicle struct {
	numberPlate string
	vehicleType VehicleType
}

func (v Vehicle) toString() string {
	return fmt.Sprintf("{NumberPlate: %s, VehicleType: %s}", v.numberPlate, v.vehicleType.toString())
}

type VehicleType uint8

const (
	car VehicleType = iota
	truck
	motorcycle
)

func (s VehicleType) toString() string {
	switch s {
	case car:
		return "Car"
	case truck:
		return "Truck"
	case motorcycle:
		return "Motorcylce"
	}
	return ""
}

func main() {

	testSmallParkingLot()

	//testLargeParkingLot()

}

func testSmallParkingLot() {
	firstParkingFloor := initParkingFloor(1)
	firstFloorEntryGate1 := initGate(1, entryGateType)
	firstFloorExitGate := initGate(1, exitGateType)
	firstFloorCarParkingSpot1 := initParkingSpot(1, carPT, "A1")
	firstFloorCarParkingSpot2 := initParkingSpot(1, carPT, "A2")

	parkingSystem := ParkingSystem{
		parkingFloor:  []*ParkingFloor{firstParkingFloor},
		entryGates:    []Gate{firstFloorEntryGate1},
		exitGates:     []Gate{firstFloorExitGate},
		issuedTickets: make(map[string]ParkingTicket),
	}
	//Add first floor parkings
	parkingSystem.addParkingSpot(firstFloorCarParkingSpot1)
	parkingSystem.addParkingSpot(firstFloorCarParkingSpot2)

	carVehicle1 := Vehicle{
		numberPlate: "C1",
		vehicleType: car,
	}
	carVehicle2 := Vehicle{
		numberPlate: "C2",
		vehicleType: car,
	}

	parkingSystem.printStatus()
	carVehicleTicket1, err := parkingSystem.issueTicket(carPT, carVehicle1, firstFloorEntryGate1)
	if err != nil {
		fmt.Println(err)
	}
	carVehicleTicket1.print()

	carVehicleTicket2, err := parkingSystem.issueTicket(carPT, carVehicle2, firstFloorEntryGate1)
	if err != nil {
		fmt.Println(err)
	}
	carVehicleTicket2.print()

	carVehicle3 := Vehicle{
		numberPlate: "C3",
		vehicleType: car,
	}
	carVehicleTicket3, err := parkingSystem.issueTicket(carPT, carVehicle3, firstFloorEntryGate1)
	if err != nil {
		fmt.Println(err)
	}
	parkingSystem.printStatus()

	parkingSystem.exitVehicle(carVehicleTicket1, cash)
	parkingSystem.printStatus()

	carVehicleTicket3, err = parkingSystem.issueTicket(carPT, carVehicle3, firstFloorEntryGate1)
	if err != nil {
		fmt.Println(err)
	}
	carVehicleTicket3.print()
	parkingSystem.printStatus()

}

func testLargeParkingLot() {
	//We have two parking floor
	firstParkingFloor := initParkingFloor(1)
	secondParkingFloor := initParkingFloor(2)

	//We have two entry gates in firstParkingFloor
	firstFloorEntryGate1 := initGate(1, entryGateType)
	firstFloorEntryGate2 := initGate(1, entryGateType)

	//We have one exit gate on firstParkingFloor
	firstFloorExitGate := initGate(1, exitGateType)

	parkingSystem := ParkingSystem{
		parkingFloor:  []*ParkingFloor{firstParkingFloor, secondParkingFloor},
		entryGates:    []Gate{firstFloorEntryGate1, firstFloorEntryGate2},
		exitGates:     []Gate{firstFloorExitGate},
		issuedTickets: make(map[string]ParkingTicket),
	}

	//We have two car parking spots, two motorcyle parking spots, 1 truck paring spot on each of the floor
	firstFloorCarParkingSpot1 := initParkingSpot(1, carPT, "A1")
	firstFloorCarParkingSpot2 := initParkingSpot(1, carPT, "A2")
	firstFloorMotorCycleParkingSpot1 := initParkingSpot(1, motorcyclePT, "A3")
	firstFloorMotorCycleParkingSpot2 := initParkingSpot(1, motorcyclePT, "A4")
	firstFloorTruckParkingSpot := initParkingSpot(1, truckPT, "A5")

	//We have two car parking spots, two motorcyle parking spots, 1 truck paring spot on each of the floor
	secondFloorCarParkingSpot1 := initParkingSpot(2, carPT, "B1")
	secondFloorCarParkingSpot2 := initParkingSpot(2, carPT, "B2")
	secondFloorMotorCycleParkingSpot1 := initParkingSpot(2, motorcyclePT, "B3")
	secondFloorMotorCycleParkingSpot2 := initParkingSpot(2, motorcyclePT, "B4")
	secondFloorTruckParkingSpot := initParkingSpot(2, truckPT, "B5")

	//Add first floor parkings
	parkingSystem.addParkingSpot(firstFloorCarParkingSpot1)
	parkingSystem.addParkingSpot(firstFloorCarParkingSpot2)
	parkingSystem.addParkingSpot(firstFloorMotorCycleParkingSpot1)
	parkingSystem.addParkingSpot(firstFloorMotorCycleParkingSpot2)
	parkingSystem.addParkingSpot(firstFloorTruckParkingSpot)

	//Add second floor parkings
	parkingSystem.addParkingSpot(secondFloorCarParkingSpot1)
	parkingSystem.addParkingSpot(secondFloorCarParkingSpot2)
	parkingSystem.addParkingSpot(secondFloorMotorCycleParkingSpot1)
	parkingSystem.addParkingSpot(secondFloorMotorCycleParkingSpot2)
	parkingSystem.addParkingSpot(secondFloorTruckParkingSpot)

	carVehicle1 := Vehicle{
		numberPlate: "C1",
		vehicleType: car,
	}
	carVehicle2 := Vehicle{
		numberPlate: "C2",
		vehicleType: car,
	}
	motorCycleVehicle1 := Vehicle{
		numberPlate: "M1",
		vehicleType: motorcycle,
	}
	motorCycleVehicle2 := Vehicle{
		numberPlate: "M2",
		vehicleType: motorcycle,
	}

	truckVehicle1 := Vehicle{
		numberPlate: "T1",
		vehicleType: motorcycle,
	}

	parkingSystem.printStatus()
	carVehicleTicket1, err := parkingSystem.issueTicket(carPT, carVehicle1, firstFloorEntryGate1)
	if err != nil {
		fmt.Println(err)
	}
	carVehicleTicket1.print()
	carVehicleTicket2, err := parkingSystem.issueTicket(carPT, carVehicle2, firstFloorEntryGate1)
	if err != nil {
		fmt.Println(err)
	}
	carVehicleTicket2.print()
	motorCycleVehicleTicket1, err := parkingSystem.issueTicket(motorcyclePT, motorCycleVehicle1, firstFloorEntryGate1)
	if err != nil {
		fmt.Println(err)
	}
	motorCycleVehicleTicket1.print()
	motorCycleVehicleTicket2, err := parkingSystem.issueTicket(motorcyclePT, motorCycleVehicle2, firstFloorEntryGate1)
	if err != nil {
		fmt.Println(err)
	}
	motorCycleVehicleTicket2.print()
	truckVehicleTicket1, err := parkingSystem.issueTicket(truckPT, truckVehicle1, firstFloorEntryGate1)
	if err != nil {
		fmt.Println(err)
	}
	truckVehicleTicket1.print()
	parkingSystem.printStatus()

	carVehicle3 := Vehicle{
		numberPlate: "C3",
		vehicleType: car,
	}
	carVehicle4 := Vehicle{
		numberPlate: "C4",
		vehicleType: car,
	}
	motorCycleVehicle3 := Vehicle{
		numberPlate: "M3",
		vehicleType: motorcycle,
	}
	motorCycleVehicle4 := Vehicle{
		numberPlate: "M4",
		vehicleType: motorcycle,
	}

	truckVehicle2 := Vehicle{
		numberPlate: "T2",
		vehicleType: motorcycle,
	}
	carVehicleTicket3, err := parkingSystem.issueTicket(carPT, carVehicle3, firstFloorEntryGate1)
	if err != nil {
		fmt.Println(err)
	}
	carVehicleTicket3.print()
	carVehicleTicket4, err := parkingSystem.issueTicket(carPT, carVehicle4, firstFloorEntryGate1)
	if err != nil {
		fmt.Println(err)
	}
	carVehicleTicket4.print()
	motorCycleVehicleTicket3, err := parkingSystem.issueTicket(motorcyclePT, motorCycleVehicle3, firstFloorEntryGate1)
	if err != nil {
		fmt.Println(err)
	}
	motorCycleVehicleTicket3.print()
	motorCycleVehicleTicket4, err := parkingSystem.issueTicket(motorcyclePT, motorCycleVehicle4, firstFloorEntryGate1)
	if err != nil {
		fmt.Println(err)
	}
	motorCycleVehicleTicket4.print()
	truckVehicleTicket2, err := parkingSystem.issueTicket(truckPT, truckVehicle2, firstFloorEntryGate1)
	if err != nil {
		fmt.Println(err)
	}
	truckVehicleTicket2.print()
	parkingSystem.printStatus()

	carVehicle5 := Vehicle{
		numberPlate: "C5",
		vehicleType: car,
	}
	carVehicleTicket5, err := parkingSystem.issueTicket(carPT, carVehicle5, firstFloorEntryGate1)
	if err != nil {
		fmt.Println(err)
	}

	parkingSystem.printStatus()

	parkingSystem.exitVehicle(carVehicleTicket1, cash)
	parkingSystem.printStatus()

	carVehicleTicket5, err = parkingSystem.issueTicket(carPT, carVehicle5, firstFloorEntryGate1)
	if err != nil {
		fmt.Println(err)
	}
	carVehicleTicket5.print()
	parkingSystem.printStatus()
}

输出

Printing Status of Parking Spot
Details of parking spots of type Car Parking Type on floor 1
Location = A2, parkingType = Car Parking Type, floor = 1 full =false
Location = A1, parkingType = Car Parking Type, floor = 1 full =false

Going to issue ticket for vehicle number C1
Issued ticket for vehicle number C1 at parking spot A2
Going to issue ticket for vehicle number C2
Issued ticket for vehicle number C2 at parking spot A1
Going to issue ticket for vehicle number C3
Cannot issue ticket. All Car Parking Type parking spot type are full

Printing Status of Parking Spot
Details of parking spots of type Car Parking Type on floor 1
Location = A2, parkingType = Car Parking Type, floor = 1 full =true
Location = A1, parkingType = Car Parking Type, floor = 1 full =true

Vehicle with number C1 exiting from Parking Lot
Paying price of 1$ through cash payment

Printing Status of Parking Spot
Details of parking spots of type Car Parking Type on floor 1
Location = A2, parkingType = Car Parking Type, floor = 1 full =true
Location = A1, parkingType = Car Parking Type, floor = 1 full =true

Going to issue ticket for vehicle number C3
Issued ticket for vehicle number C3 at parking spot A2
Printing Status of Parking Spot
Details of parking spots of type Car Parking Type on floor 1
Location = A1, parkingType = Car Parking Type, floor = 1 full =true
Location = A2, parkingType = Car Parking Type, floor = 1 full =true

结论

这篇文章的内容是关于设计一个停车场的,希望你喜欢这篇文章。如果有任何反馈,请在评论中分享。

检查文件是否在 Ruby 语言中存在

原文:techbyexample.com/file-exists-ruby/

概述

Ruby 中的 File 类提供了一种方法,如果给定文件存在,则返回 true;如果给定文件不存在,则返回 false。

ruby-doc.org/core-3.0.0/File.html#method-c-exist-3F

这是该方法的签名

exist?(file_name) → true or false

示例

以下是相应的程序。

File.open("temp.txt", "w") do |f|     
    f.write("Test")   
  end

puts File.exist?('temp.txt')

输出

true

注意: 查看我们的系统设计教程系列 系统设计问题

Instagram 系统设计

原文:techbyexample.com/instagram-system-design/

目录

  • 概述

  • 功能需求

  • 非功能需求

  • 用户 API

  • 数据存储

  • 使用什么数据库

    • 如何上传照片和视频

    • 如何存储点赞和评论

  • 发布状态更新

  • 如何生成时间线

    • 不同的时间线生成方法

    • 第一种方法 – 在运行时获取更新

    • 第二种方法 – 使用拉取方式生成时间线(从数据库获取)

    • 第三种方法 – 使用推送方式生成时间线

    • 第四种方法 – 在更新可用时将其推送到客户端

  • 其他常见组件

  • 非功能需求

    • 可扩展性

    • 低延迟

    • 可用性

    • 警报和监控

    • 向用户位置靠近

    • 避免单点故障

  • 结论

概述

Instagram 是最受欢迎的社交媒体分享应用之一,允许用户与其他用户分享他们的照片和视频。用户可以点赞和评论其他用户的照片和视频。用户可以互相关注,并查看他们关注的用户的新闻动态。

功能需求

Instagram 设计的一些功能需求包括:

  • 用户可以发布带有图片或视频的状态更新

  • 为用户生成时间轴,其中将包含该用户的新闻提要

  • 用户将能够点赞或评论帖子

  • 允许一层评论嵌套

  • 用户应能够互相关注或取消关注

非功能性需求

系统的一些非功能性需求包括

  • 在像 Instagram 这样的系统中,人们查看照片和新闻提要的频率远高于上传照片。因此,这是一个读重的系统。我们需要以最小延迟设计系统。读写比为 80:20

  • 系统应具备高度可用性,能够服务 5 亿用户

  • 系统应该是持久的。任何上传到系统的图片、视频或帖子都必须始终存在,除非用户自己删除它们

  • 系统需要最终一致性。这意味着一旦用户上传了任何照片,过一段时间后,它将在其关注者的时间轴中可见

用户 API

以下是所需的 API

  • 创建一个带照片或视频的帖子

  • 评论一个帖子

  • 对评论本身进行评论

  • 点赞帖子

  • 点赞评论

  • 获取时间轴

数据存储

对于数据存储,我们需要存储以下内容

  • 图像和视频

  • 帖子

  • 评论和点赞

  • 关注者

  • 新闻提要

选择什么数据库

  • 对于 Instagram,我们不需要满足 ACID 要求,且数据量将非常庞大,因此我们需要使用 No SQL 数据库

  • 系统将是读重的。

我们可以使用 Cassandra 数据库,因为它是一个 No SQL 数据库,可以存储大量数据,同时也能处理高读写量。

现在,让我们看一下每个元素存储的数据模型

照片和视频如何上传

存储图像和视频需要便宜的存储空间,可以使用文件系统。我们可以使用 Amazon S3 或 HDFS。进一步可以使用 CDN 来缓存图像和视频。

点赞和评论如何存储

首先,让我们列出所有与点赞和评论相关的需求

  • 帖子可以有任意数量的点赞

  • 帖子可以有任意数量的评论

  • 你可以点赞帖子,也可以点赞评论

  • 允许一层评论嵌套

为了简化,我们将有

  • 有两个点赞表,一个是post_like,另一个是comment_like

  • 一个帖子表,

  • 一个评论表。

以下是所有的表格。

帖子表

以下是帖子表中的字段。此表将根据user_id进行分区。

  • post_id

  • 标题

  • 描述

  • tags – 该字段将是哈希值

  • 缩略图

  • user_id

  • 创建时间

  • 更新

  • image_id

该表将根据 user_id 进行分片,以便我们能从单个分片中访问用户的所有帖子评论表

以下是评论表中的字段。此表将根据post_id进行分区。这样做是为了确保与某个帖子的所有评论都在同一个分片中

  • comment_id

  • 评论 – 这是一个文本字段

  • post_id

  • user_id

  • 创建时间

  • 更新

  • parent_id – 这个字段用于处理评论的嵌套结构。

这个表应该根据 post_id 进行分片,以便我们能够从一个分片中获取属于某个帖子的所有评论。

post_like 表

Instagram 会显示你是否已点赞某个帖子,同时也会显示哪些用户点赞了某个特定的帖子。所有这些信息都会存储在这个表中。这个表将根据 post_id 进行分片,以便你可以通过单一分片获取与某个帖子相关的所有点赞。

以下是Post_Like 表中的字段。

Post_Like 表

  • id

  • user_id

  • post_id

  • created

  • updated

这个表应该根据 post_id 进行分片,以便我们能够从一个分片中获取属于某个帖子的所有点赞。如何获取某个帖子的点赞数,我们可以直接查询这个表来获取该信息。该统计信息也可以存储在缓存中。每当对帖子进行点赞时,缓存就会被更新。另一种方式是每次有点赞时就刷新缓存。

你可以拥有一个独立的服务,这可以是一个监听特定主题的工作者,当点赞事件发生时,它会被发布到该主题。该工作者将更新或失效缓存。comment_like 表

以下是 Comment_Like 表中的字段。这个表也会根据 post_id 进行分区。这样做是为了确保与某个帖子的评论相关的所有点赞都能存储在一个分片中。

  • id

  • user_id

  • post_id

  • comment_id

  • created

  • updated

这个表应该根据 comment_id 进行分片,以便我们能够从一个分片中获取属于某个评论的所有点赞。

如何获取某个评论的点赞数。我们可以直接查询这个表来获取相关信息。与post_like表类似,我们也可以将这些信息存储在某种缓存中。

粉丝和相关数据将如何存储。

为此,将会有一个Follow表。以下是Follow表中的字段。

  • user_id

  • follower_user_id

  • created

  • updated

新闻推送将如何存储

当我们讨论如何生成新闻推送时,会讨论新闻推送的存储方式。

高级设计

从高层次上讨论一下更高的流程以及会存在的所有服务。

  • 将会有一个API 网关,所有用户的请求都会先到达这个网关。

  • 将会有一个User service,它将存储用户的个人信息。

  • 将会有一个Token service,它将生成并验证令牌。基本上,它会处理与令牌管理相关的所有事务。

  • 将会有一个Post Service,所有与帖子相关的请求都会被接收。

  • Post service 首先会在数据库中为帖子在帖子表中创建一条记录。

  • 帖子在数据库中创建后,会将消息发送到Kafka + SNS/SQS 系统

  • 这条消息将被时间线初始化服务(Timeline_Init Service)处理,这个工人将向关注者服务(Follower Service)发起调用,以获取该帖子所有者的所有关注者。然后它将把消息分发到每个关注者的Kafka + SNS/SQS 系统

  • 每个分发消息将由另一个工人处理,这个工人将是时间线创建工人(Timeline_Create Worker)。它将为用户创建时间线。稍后在本教程中,我们将研究生成动态的不同方式和场景。

  • 将会有一个关注者服务(Follower Service)。当任何用户关注某个用户时,调用将进入该服务。该服务将会在数据库中创建一条记录,并将消息推送到Kafka + SNS/SQS 系统

  • 这条消息将被通知服务(Notification Service)处理,该服务将是一个工人。这个通知工人(Notification Worker)将向被关注的用户发送通知。

  • 还将有一个反馈服务(Feedback Service),该服务将处理所有与点赞帖子或评论、评论帖子或评论本身相关的 API 调用。同样,一旦接收到此类活动,它将发布一条消息到 Kafka + SNS 系统。

  • 这条消息将被通知服务(Notification Service)处理,作为一个工人,它将向帖子所有者或评论所有者(根据适用情况)发送通知。

  • 这条消息也会被另一个工人处理,名为反馈计数器工人(Feedback_Counter)。该工人将增加评论或帖子(取决于情况)上的点赞数。计数将会在缓存中增加。

让我们详细讨论每个流程,并为每个流程绘制一个图示。

发布状态更新

如前所述,当有人创建帖子时,帖子服务将介入。帖子可能包含要上传的照片或视频。让我们看看这些图片和视频是如何上传的。对于图片和视频的上传,我们可以假设不会上传原始大小的图片或视频。客户端将创建一个低分辨率版本,然后上传。即使是任何图像和视频的低分辨率版本也只有几 KB。它们可以直接上传到存储提供商。例如,如果存储提供商是 AWS S3,那么以下就是流程:

  • 假设用户 A 在其 Instagram 客户端上想要发布一个包含图片或视频的状态。客户端将向服务器发送请求,获取一个预签名的 URL,以便客户端可以上传图片或视频。

  • 服务器将响应一个预签名的 URL,其有效期可能是几个小时。你可以阅读这篇文章来了解预签名 URL 的概念。请参考这份文档了解更多关于预签名 URL 的信息——docs.aws.amazon.com/AmazonS3/latest/userguide/ShareObjectPreSignedURL.html。基本上,它是一个已经用令牌签名的 URL,因此可以直接上传到存储提供者 S3,而无需进一步的身份验证。这也叫做直接上传。服务器还会返回此处的 image_id。

  • 客户端将把图像上传到该 URL。它将直接存储在 S3 中。

  • 现在客户端将请求创建包含在前面步骤中上传的图像或视频的 POST 请求。它还会发送上传视频和照片的 ID。

  • 该请求将被Post 服务接收,Post 服务将在数据库中的帖子表中创建一个条目。

  • 然后它将消息发送到Kafka + SNS/SQS 系统

  • 该消息将由时间轴服务处理,包括timeline_Inittimeline_create工作器。它们将创建时间轴。

  • Post 服务还将把新创建的帖子缓存到分布式 Redis 中

一条帖子也可能包含视频。我们可以对视频流进行一个优化。对于视频流,我们有两个要求。

  • 上传的视频应适合在多种设备上查看。

  • 世界各地的人们网络速度不同。因此,自适应比特率流意味着根据网络速度选择不同的分辨率,以避免缓冲。

为了满足上述两个要求,我们可以做以下几件事

  • 将视频转码为不同格式

  • 将每个转码视频转换为不同的分辨率。不同的分辨率可以是 144p、480p、1080p。

可能会有与视频管理相关的独立服务来执行上述操作。

以下是该过程的高层次图示

时间轴将如何生成

让我们来看一下用户时间轴是如何生成的。用户的时间轴将提前在缓存中更新。我们已经看到有人发布状态时发生了什么。现在让我们看看之后会发生什么,以便生成用户时间轴。

首先,让我们探索时间轴生成的不同方法。

时间轴生成的不同方法

生成时间轴的主要方法有四种

  • 在运行时获取更新(客户端到服务器)

  • 使用拉取方法生成时间轴预生成(从数据库提取并从客户端到服务器)

  • 使用推送方法生成时间轴预生成(事件驱动和客户端到服务器)

  • 当有更新时推送(服务器到客户端)

我们将结合这些方法来最终生成用户的时间线,具体取决于

  • 用户是活跃用户还是非活跃用户

  • 用户关注其他名人,这些名人可能有数百万粉丝。

所有这些方法都将依赖于一个时间戳值,用来获取每个用户的时间线。这个时间戳将用于构建时间线,以基于用户已经看到的更新。如果用户的时间戳是 2022 年 2 月 23 日 11:15 GMT,那么意味着他已经看到所有在这个时间戳之前的更新。当请求到达服务器时,它只会返回该时间戳之后的时间线或更新。

现在我们来讨论这四种方法。但在讨论这些方法之前,先让我们了解一下用户场景。

用户 X 关注以下用户。

  • A

  • B

  • C

  • D

  • E

  • F

下面是更多数据

*A、B、C、D是拥有数百个粉丝的普通用户

  • E是一个拥有数百万粉丝的名人

  • F是一个商业账户,也有数百万粉丝。

第一种方法 – 运行时获取更新

在第一种方法中,客户端应用程序将向服务器发起请求。服务将在运行时获取客户的所有更新。

向服务器发起拉取请求将获取当前用户的时间戳。假设这个时间戳为t1。然后,它将调用 POST 服务,获取在t1之后创建的 A、B、C、D、E 和 F 用户的更新数据。获取到这些数据后,它会将其发送给客户端。下面是相应的示意图。

这种方法存在多个问题—

  • 首先,这是一种耗时的方法。为什么呢?因为它将获取

  • 第二,它将从所有用户那里获取更新,我们已经提到过,帖子表是基于 user_id 共享的。因此,这个查询将涉及多个分片,从而带来性能开销。

  • 这种方法不可扩展

第二种方法 – 使用拉取方法生成时间线(从数据库获取)

在第二种方法中,将有一个额外的timeline_create服务,该服务会提前创建用户的时间线。这个任务会定期为每个用户调用。它会获取当前用户的时间戳。假设这个时间戳为t1。然后,它将调用数据库,获取在t1之后创建的 A、B、C、D、E 和 F 用户的更新数据。获取到更新后,它将把这些数据插入某种数据库或缓存中。我们将其称为时间线数据库或时间线缓存。下面是该数据库的模式。

  • user_id

  • post_id

  • 创建

  • 更新

下面是相应的示意图。

这种方法的一些问题

  • 可能用户没有新的更新。因此,即使我们尝试查找该用户的新帖子,但有几次结果为空。

  • 我们定期获取并更新用户的时间线。很可能用户根本不活跃,我们可能仍在生成该用户的时间线。

第三种方法——使用推送方法生成时间线

在这种方法中

  • 用户发布任何状态更新时,会在 SNS/Kafka 上发布一条消息。

  • 该消息由 timeline_init 服务处理,它是一个工作者。

  • timeline_init 服务会获取前 100 批粉丝。对于每个粉丝,它会再次将消息分发给 timeline_create 服务,它也是一个工作者。此过程会对所有粉丝重复进行。

  • timeline_create 服务接收到消息后,会使用该 post_id 更新粉丝的时间线。

以下是相同方法的示意图。

这种方法的问题

  • 名人或商业账户可能会有大量粉丝。当这些账户发布状态时,需要更新所有百万级粉丝的时间线。这种做法效率低下。

  • 同样,类似于方法 2,我们可能会更新一个不活跃用户的时间线。

第四种方法——在更新可用时推送到客户端

这是服务器到客户端的通信。它也是事件驱动的。只不过,一旦更新可用,它会从服务器推送到客户端。所以用户没有预先生成的时间线,因为更新是直接从服务器推送到客户端的。这种服务器到客户端的通信需要不同类型的协议,如 WebSockets。

以下是此方法的示意图。

这种方法的问题

*** 服务器到客户端的通信需要套接字或任何其他类似的技术。这些技术在资源消耗方面很昂贵,并且需要保持持久的连接。

  • 如果客户端处于离线状态怎么办?在这种情况下,当服务器推送更新时,如果发现客户端离线,服务器可以将更新保存到数据库并进行缓存。

  • WebSockets 对于一些实时性要求很高的应用(例如 WhatsApp)非常有用,但可能不太适合 Instagram。因此,如果 Instagram 使用 WebSockets,可以采用这种方法。

推荐方法

如上所示,每种方法都有一些缺点。因此,用户的时间线生成将取决于天气。

  • 用户是活跃用户

  • 用户是非活跃用户

用户是活跃用户

在这种情况下,我们可以结合使用方法 1 和方法 3。

  • 使用方法 3,我们可以为该用户生成时间线,只针对那些拥有数百个粉丝的账户。

  • 然后在运行时,我们可以从所有名人和商业账户获取更新,并将其与步骤 1 中生成的时间线合并。

让我们通过一个示例来理解上面的内容。正如我们之前提到的,用户 X 关注了以下用户

  • A – A1->A2

  • B – B1

  • C – C1

  • D – D1->D2

  • E – E1

  • F – F1->F2

  • A、B、C、D 是普通用户,他们的粉丝数为数百

  • E 是一位拥有数百万粉丝的名人

  • F 是一个商业账户,也有数百万的粉丝

所以,当用户是活跃用户时,下面是时间线生成的方式。

  • A、B、C、D 是普通用户,他们的粉丝数为数百。因此,用户 X 的时间线将使用方法 3 生成,包含这些用户的帖子。所以用户 X 的时间线将如下生成并保存在缓存中

A1->A2->B2->C1->D1-D2

  • 用户X的时间线不会包含来自用户E和用户F的帖子

  • 当用户调用获取时间线时,运行时会从用户 E 和 F 获取更新/帖子,然后将其与来自A、B、C、D用户已生成的时间线合并并返回。

用户不是活跃用户

在这种情况下,我们只能使用方法 1。由于用户根本不是活跃用户,只是每周打开一次应用程序,因此事先为该用户生成时间线没有意义,因为这会浪费存储空间。所以在这种情况下,当用户调用获取时间线时,运行时将从A、B、C、D、E、F用户获取更新/帖子。

但如果一个用户有大量粉丝,这样做似乎并不高效。这时,方法 2 就派上了用场。当用户上线时,我们可以使用方法 1 从其部分粉丝处获取更新。然后它可以触发后台任务,使用方法 2 为用户生成剩余的时间线。以下是一个示例来帮助理解

  • 当用户调用获取时间线时,运行时会从A、B、C用户获取更新/帖子

  • 会触发一个后台任务,使用方法 2 为用户生成剩余的时间线,获取来自D、E、F的更新/帖子。当用户观看第一组更新时,第二组更新可以从生成的时间线中提取。

所以总体来说,方法 2 可以在运行时用来为用户生成时间线。当方法 1 非常昂贵时,也可以优先使用方法 2。下面是时间线生成的高层次示意图。假设有两个用户 X 和 Y,Y 是 X 的粉丝,Y 还关注其他名人用户。在下面的高层次图中,我们结合了方法 1 和方法 3。

*** 使用方法 1,它从用户 Y 关注的名人用户获取更新

  • 使用方法 3,它会预先为用户 Y 创建时间线。此时间线是通过用户 Y 关注的非名人用户的帖子生成的。

以下是高层次流程

对于用户 A

  • 用户 A 想要发布带有图片的帖子。它调用 POST 服务来获取签名的 URL。然后它使用签名的 URL 直接上传到 tS3。

  • 然后,它直接调用 POST 服务来创建帖子。该帖子会同时保存在数据库和缓存中。

  • 在帖子创建到数据库后,系统会将消息发送到 Kafka + SNS/SQS 系统

  • 这条消息将被 Timeline_Init Service 拾取,这个服务是一个工作者,它将调用 Follower Service 来获取发布者的所有粉丝。然后它会将消息广播到每个粉丝,并发送到 Kafka + SNS/SQS 系统

  • 每个广播的消息将由另一个工作者处理,这个工作者将是 Timeline_Create Worker。它将为用户 B 创建新闻推送。这就是时间线生成的第三种方法发挥作用的地方。

对于用户 B

  • 用户 B 想要获取他的时间线。

  • Instagram 调用 Timeline App 服务。Timeline App 服务做两件事。

  • 首先,它调用关注者服务来获取用户 Y 关注的名人用户。然后,它从 POST 服务中获取这些名人用户的更新。此时方法 1 就开始发挥作用。

  • 其次,它从缓存和数据库中获取用户 Y 的预先创建的时间线。

  • 它将两者的结果合并,并返回给 Instagram 客户端,以供用户 Y 使用。

  • Instagram 然后使用返回的时间线中的 URL 直接从 S3 下载图片/视频。我们也可以将照片/视频缓存到 CDN 上,这样可以更快地检索媒体内容。

这就是 Instagram 中时间线生成的全部内容。

其他常见组件

其他常见的组件可能包括:

  • 用户服务 – 存储用户的个人信息。

  • 令牌/认证服务 – 管理用户令牌。

  • 短信服务 – 用于向用户发送任何类型的消息。例如 – OTP(一次性密码)。

  • 分析服务 – 这个服务可以用来追踪任何类型的分析数据。

非功能性需求

现在我们来讨论一些非功能性需求。

可扩展性

在上述设计中,首先要考虑的是可扩展性因素。系统中每个组件的可扩展性都非常重要。以下是你可能遇到的可扩展性挑战及其可能的解决方案:

  • 每台 Post ServiceTimeline Service 等机器只能处理有限数量的请求。因此,每个服务应该配置合适的自动扩展,以便根据请求数量动态增加实例并在需要时进行自动扩展。

  • 你的 Kafka/SNS 系统可能无法承受如此大的负载。我们可以横向扩展,但也有一定的限制。如果它成为瓶颈,那么根据地理位置或用户 ID,我们可以拥有两个或更多这样的系统。可以使用服务发现来确定请求应该发送到哪个 Kafka 系统。

  • 可扩展性的另一个重要因素是我们设计系统的方式,确保没有服务被过多任务拖慢。我们进行关注点分离,且在某个服务承担过多责任时,我们会将其拆解。

  • 可扩展性的另一个因素是分片。需要存储大量数据,而显然这些数据无法存储在单一机器上。最好将数据分区存储在不同的机器上,从而保证整体架构的可扩展性。我们需要根据存储预估选择分片的数量,并且要智能地选择分片键或分区键,以确保没有查询会跨多个分片。

低延迟

  • 我们可以缓存新创建的帖子,并设置一定的过期时间。当然,每当一个帖子被创建时,它更有可能出现在其他用户的时间线中。这将减少许多读取请求的延迟。

  • 我们在这里做的另一个延迟优化是将用户的时间线缓存,而不是直接保存到数据库。通过缓存,时间线可以更快速地返回。

  • 我们可以将照片和视频缓存到 CDN 中,这将加速媒体的检索。

  • 进一步提高延迟的一个领域是优化视频流。

可用性

为了使系统具有高可用性,几乎所有系统组件都需要具备冗余和备份。以下是一些需要完成的任务。

  • 对于我们的数据库,我们需要启用复制。每个主分片节点应该有多个从节点。

  • 对于 Redis,我们还需要进行复制。

  • 为了确保数据冗余,我们也可以采用多区域架构。如果某个区域出现故障,这可能是其中的一个好处。

  • 也可以设置灾难恢复

告警与监控

告警与监控是一个非常重要的非功能需求。我们应该监控每个服务并设置适当的警报。可以监控的一些内容包括:

  • API 响应时间

  • 内存消耗

  • CPU 消耗

  • 磁盘空间消耗

  • 队列长度

  • ….

靠近用户位置

这里有几种架构可以遵循,其中一种是单元架构。你可以在这里了解更多关于单元架构的信息 – github.com/wso2/reference-architecture/blob/master/reference-architecture-cell-based.md

避免单点故障

单点故障指的是系统中的某个部分,一旦停止工作,将导致整个系统崩溃。我们应尽量避免设计中的单点故障。通过冗余和采用多区域架构,我们可以防止这种情况发生。

结论

这篇文章主要讨论了 Instagram 的系统设计。希望你喜欢这篇文章。请在评论中分享你的反馈******

在终端中一起搜索两个词

原文:techbyexample.com/grep-two-words-together-terminal/

目录

  • 概览

  • 方法 1

  • 方法 2

  • 方法 3

概览

这里有一些示例来实现这一操作

首先,让我们创建一个 sample.txt 文件

sample.txt

This file
contains word1 and
also word2

这里有一些使用 grep 搜索两个词的方法

方法 1

  • 使用 | 作为需要搜索的两个词之间的分隔符

示例

some_user@macOS grep 'word1\|word2' ./sample.txt
contains word1 and
also word2

方法 2

  • 使用 -e 为每个需要搜索的词

示例

some_user@macOS grep -e word1 -e word2 ./sample.txt
contains word1 and
also word2

方法 3

  • 使用 -E|

示例

some_user@macOS grep -E 'word1|word2' ./sample.txt
contains word1 and
also word2

注意: 请查看我们的系统设计教程系列 系统设计问题

如何在 MAC OS 中将文件夹设为收藏夹

原文:techbyexample.com/folder-favorite/

概述

只需将该文件夹拖动到收藏夹中。以下是步骤

  • 进入该文件夹。假设该文件夹名为 wedding,位于 ~/Desktop 位置。

    • 点击 Dock 中的 Finder

    • 进入菜单 Go->Go To Folder,然后输入 ~/Desktop

  • 现在将wedding文件夹拖到收藏夹中。如下图所示,两张图片。

就是这样。你完成了

注意: 请查看我们的系统设计教程系列 系统设计问题

zsh: 找不到匹配项:HEAD^

原文:techbyexample.com/zsh-no-matches-found-head/

概述

^ 字符是 zsh 中文件名扩展的特殊字符。有几种方法可以解决这个问题

解决方法 1

转义 ^ 字符

示例

git reset HEAD\^ --soft

或使用引号

git reset 'HEAD^' --soft

解决方法 2(永久修复)

这是由于EXTENDED_GLOB选项,zsh 允许 ^ 否定文件模式。正如这里引用的那样

zsh.sourceforge.io/Doc/Release/Options.html

EXTENDED_GLOB

将‘#’,‘’和‘^’字符视为文件名生成等模式的一部分。(一个未加引号的初始‘’总是会生成命名目录扩展。)

所以只需在终端中取消设置此选项

unsetopt EXTENDED_GLOB
git reset HEAD\^ --soft

要永久修复,可以在 .zshrc 文件中写下这一行。这告诉 .zsh 在模式匹配失败时不要打印错误,并按原样使用命令

unsetopt NOMATCH

PS:在 .zshrc 中取消设置 EXTENDED_GLOB 不是一个好主意,否则你将无法使用该行为。当关闭 NOMATCH 时,它在模式匹配失败时并不会简单地打印错误。

NOMATCH 的描述可以在这里找到

zsh.sourceforge.io/Doc/Release/Options.html

NOMATCH (+3)

如果文件名生成的模式没有匹配项,则打印错误,而不是让其在参数列表中保持不变。这也适用于初始‘~’或‘=’的文件扩展。

注意: 查看我们的系统设计教程系列 系统设计问题

修复 Ruby 2.7.0 中的 LoadError:无法加载文件 — scanf

原文:techbyexample.com/scanf-load-error-ruby/

概述

scanf gem 不再是 Ruby 安装时随附的捆绑 gem。它必须单独安装。只需将以下行添加到你的 Gemfile 中,然后运行‘bundle’

gem 'scanf'

或者运行

gem install scanf

注意: 请查看我们的系统设计教程系列 系统设计问题

修复 Ruby 2.7.0 中的 LoadError 无法加载文件 e2mmap

原文:techbyexample.com/e2mmap-load-error-ruby/

概述

e2mmap gem 不再是随 Ruby 安装捆绑的 gem,它需要单独安装。只需将下面的代码添加到你的 Gemfile 中,然后运行‘bundle’。

gem 'e2mmap'

或者运行

gem install e2mmap

注意: 查看我们的系统设计教程系列 系统设计问题

在 Ruby 语言中删除哈希中的嵌套键

原文:techbyexample.com/ruby-hash-nested-delete/

概述

在 Ruby 语言中,可以删除哈希中的嵌套键。

让我们来看一些例子

一级嵌套键

这是删除哈希中第一层键的代码

sample = {
    "a" => "b",
    "c" => "d"
}
sample.delete("a")
puts sample

输出

{"c"=>"d"}

二级嵌套键

这是删除哈希中第一层键的代码

sample = {
    "a" =>  {
        "b" => "c"
    }

}
sample["a"].delete("b")
puts sample

输出

{"a"=>{}}

三级嵌套键

这是删除哈希中第一层键的代码

sample = {
    "a" =>  {
        "b" => {
            "c" => "d"
        }
    }

}
sample["a"]["b"].delete("c")
puts sample

输出

{"a"=>{"b"=>{}}}

注意: 请查看我们的系统设计教程系列 系统设计问题

Ruby 正则匹配在 if 检查中的应用

原文:techbyexample.com/ruby-regex-match-if/

概述

我们可以使用以下符号在 if 检查中进行 ruby 正则匹配

=~

程序

让我们来看一个相同的示例

if /\d/ =~ "3" 
    puts "Match"
else 
    puts "No match"
end

输出

Match

注意: 查看我们的系统设计教程系列 系统设计问题

通过删除找到字典中最长单词的程序

原文:techbyexample.com/longest-word-dictionary/

概述

给定一个字符串和一个单词字典。目标是找到字典中作为子序列出现在给定字符串中的最长单词。如果可能的结果数量超过 1,则返回字典序最小的最长单词。

示例 1

s = "mbacnago", dictionary = ["ale","mango","monkey","plea"]
Output: "mango"

示例 2

s = "mbacnago", dictionary = ["ba","ag"]
Output: "ag"

程序

以下是相同程序的代码

package main

import (
	"fmt"
	"sort"
)

func findLongestWord(s string, dictionary []string) string {
	sort.Slice(dictionary, func(i, j int) bool {
		lenI := len(dictionary[i])
		lenJ := len(dictionary[j])

		if lenI == lenJ {
			return dictionary[i] < dictionary[j]
		}

		return lenI > lenJ
	})

	lenS := len(s)

	for i := 0; i < len(dictionary); i++ {
		if isSubstring(s, dictionary[i], lenS) {
			return dictionary[i]
		}
	}

	return ""
}

func isSubstring(s string, sub string, lenS int) bool {
	lenSub := len(sub)

	if lenSub == 0 {
		return true
	}

	if lenSub > lenS {
		return false
	}

	for i, j := 0, 0; i < lenS && j < lenSub; {
		if i+lenSub-j-1 >= lenS {
			return false
		}
		if s[i] == sub[j] {
			j++
		}
		if j == lenSub {
			return true
		}
		i++
	}

	return false
}

func main() {
	output := findLongestWord("mbacnago", []string{"ale", "mango", "monkey", "plea"})
	fmt.Println(output)

	output = findLongestWord("mbacnago", []string{"ba", "ag"})
	fmt.Println(output)

}

输出

mango
ag

此外,查看我们的系统设计教程系列 – 系统设计教程系列

在 Ruby 语言中同时遍历三个数组

原文:techbyexample.com/traverse-three-arrays-ruby/

概述

在 Ruby 中,可以同时遍历三个数组。可以使用 .zip 函数来实现。以下是 zip 函数的文档链接

ruby-doc.org/core-2.0.0/Array.html#method-i-zip

让我们来看一下相应的程序

程序

下面是当三个数组的大小相同时,遍历三个数组的代码

a = ["d", "e", "f"]
b = ["g", "h", "i"]

["a", "b", "c"].zip(a, b) do |x,y,z| 
   puts x+ " " + y + " " + z 
end

输出

a d g
b e h
c f i

如果数组的大小不相同,则会为较小数组中的元素打印空白空间

a = ["d", "e"]
b = ["g"]

["a", "b", "c"].zip(a, b) do |x,y,z| 
   puts x
   puts y
   puts z
end

输出

a
d
g
b
e

c 

注意: 查看我们的系统设计教程系列 系统设计问题

删除链表元素程序

原文:techbyexample.com/remove-linked-list-elements-program/

概述

给定一个链表和一个值,删除链表中所有值等于给定值的节点。

示例 1

Input: [1, 2, 1, 3, 6], 1
Output: [2, 3, 6]

示例 2

Input: [2, 2, 3], 2
Output: [3]

程序

以下是相同程序的代码

package main

import "fmt"

type ListNode struct {
	Val  int
	Next *ListNode
}

type SingleList struct {
	Len  int
	Head *ListNode
}

func (s *SingleList) AddFront(num int) *ListNode {
	ele := &ListNode{
		Val: num,
	}
	if s.Head == nil {
		s.Head = ele
	} else {
		ele.Next = s.Head
		s.Head = ele
	}
	s.Len++
	return ele
}
func removeElements(head *ListNode, val int) *ListNode {
	var prev *ListNode

	curr := head

	for curr != nil {
		if curr.Val == val {
			if prev == nil {
				head = curr.Next
			} else {
				prev.Next = curr.Next
			}
		} else {
			prev = curr
		}
		curr = curr.Next

	}

	return head
}

func main() {
	first := initList()
	first.AddFront(6)
	first.AddFront(3)
	first.AddFront(1)
	first.AddFront(2)
	first.AddFront(1)

	result := removeElements(first.Head, 1)
	fmt.Println("Resultant First List")
	result.Traverse()

	first = initList()
	first.AddFront(3)
	first.AddFront(2)
	first.AddFront(2)

	fmt.Println("\nResultant Second List")
	result = removeElements(first.Head, 2)
	result.Traverse()

}

func initList() *SingleList {
	return &SingleList{}
}

func (l *ListNode) Traverse() {
	for l != nil {
		fmt.Println(l.Val)
		l = l.Next
	}
}

输出

Resultant First List
2
3
6

Resultant Second List
3

注意: 查看我们的 Golang 高级教程。本系列教程内容详细,我们尽力通过示例覆盖所有概念。本教程适合那些希望在 Golang 上获得专业技能并深入理解的读者——Golang 高级教程

如果你有兴趣了解如何在 Golang 中实现所有设计模式。如果是的话,那么这篇文章适合你——所有设计模式 Golang

理解基于目录的分片

原文:techbyexample.com/directory-based-sharding/

目录

  • 概述

  • 何时使用基于目录的分片

  • 基于目录的分片的缺点

  • 热分片问题

    • 解决方案 1

    • 解决方案 2

  • 结论

概述

在本教程中,我们将尝试理解基于目录的分片。除了基于目录的分片外,还有两种其他类型的分片

  • 基于键的分片

  • 基于范围的分片

基于目录的分片保持一个静态查找表,用来追踪哪些分片存储了哪些数据。让我们考虑一个例子。假设你需要跟踪美国不同地区的所有餐馆。每个餐馆将属于美国的某个特定位置。我们可以将美国划分为多个区域,每个区域将存储在一个独立的分片中。

餐馆表

餐馆名称 区域
R1 ZA
R2 ZB
R3 ZC
R4 ZA
R5 ZD
R6 ZB

现在我们将维护一个查找表,用于存储区域与分片的映射关系

查找表

区域 分片
ZA 1
ZB 2
ZC 3
ZD 4

很有可能两个区域会存储在同一个分片中,这是允许的。这个查找表通常保存在应用程序和数据库之外

何时使用基于目录的分片。

  • 当查找表基于的列的基数较低时。

例如,在上述情况中,我们将在美国范围内有固定数量的区域。即使存在一百万条餐馆记录,区域的数量也是固定的。因此,餐馆表中的区域列的基数较低。

作为反例,我们不能使用查找表中的employee_id,因为如果有比如说 2 万个员工,那么就会有 2 万个不同的employee_id,这会导致查找表的大小是无限的,因为这里employee_id列的基数较高

另一个重要因素是,除了列的基数较低外,列的值也不应该发生变化。例如,考虑一个订单表。一个订单会有不同的状态

  • 初始化

  • 成功

  • 失败

  • 已发货

在这里,状态列的基数较低,但订单可以从初始化状态变为成功失败。从成功状态,它可以变为已发货。因此,状态列不是适合用作查找表的字段,否则,订单的状态变化时,订单会从一个分片转移到另一个分片。

基于目录的分片的缺点

  • 查找表的开销

每次我们想知道用户数据属于哪个分片时,都需要查询查找表。这种查找操作会在每次读写时发生。

  • 查找表作为单点故障

查找表可以保存在一个单独的位置,可能是 Redis 或其他数据库。这个查找表可能成为单点故障。因此,我们必须维护多个查找表的副本。

热门分片问题

像其他任何分片技术一样,这种分片方法也可能导致几个热门分片。所以通常在做基于目录的分片时,分片键的选择是让数据在各个分片之间均匀分布。在上述例子中,我们可以认为某些区域可能包含比其他区域更多的餐馆。在这种情况下,我们可以通过两种方式来解决这个问题。

解决方案 1

  • 将一些餐馆数量较少的区域映射到同一个分片。但将餐馆数量较多的区域映射到一个分片。例如,假设在上面的例子中,ZAZB有大量餐馆,而ZCZD的餐馆较少。那么查找表可能会是这样的。

查找表

区域 分片
ZA 1
ZB 2
ZC 3
ZD 3

解决方案 2

使用混合方法。我们可以通过区域和子区域的组合来决定分片。例如,ZA 包含了很多餐馆,无法简单地放入一个分片中。此时,区域和子区域的组合将决定分片。

餐馆表

餐馆名称 区域 子区域
R1 ZA X
R2 ZB X
R3 ZC X
R4 ZA Y
R5 ZD X
R6 ZB Y
区域 分片
ZA_X 1
ZA_Y 2
ZB_X 3
ZB_Y 4
ZC_X 5
ZC_Y 5
ZD_X 6
ZD_Y 6

区域ZAZB的子区域映射到不同的分片。而区域ZCZD的子区域映射到同一个分片。

结论

这完全是基于目录的分片。希望你喜欢这篇文章。

计算未被守卫的单元程序

原文:techbyexample.com/count-unguarded-cells-program/

概述

给定两个整数 m 和 n,表示一个 m*n 的网格。此外,还给定了两个二维整数数组

  • guards,其中 guards[i] = [rowi, columni]。它表示第 i 个守卫的位置

  • walls,其中 guards[j] = [rowi, columni]。它表示第 j 个墙的位置

一个守卫可以看到所有方向

  • 西

除非被墙阻挡。所有守卫能够看到的单元都会被视为已守卫。目标是找出未被守卫的单元数量

这是相同题目的 Leetcode 链接 – https://leetcode.com/problems/count-unguarded-cells-in-the-grid/

程序

以下是相同程序的代码

package main

import "fmt"

func countUnguarded(m int, n int, guards [][]int, walls [][]int) int {

	occupancy := make([][]int, m)

	for i := 0; i < m; i++ {
		occupancy[i] = make([]int, n)
	}

	for _, val := range guards {
		i := val[0]
		j := val[1]
		occupancy[i][j] = 2
	}

	for _, val := range walls {
		i := val[0]
		j := val[1]
		occupancy[i][j] = -1
	}

	for i := 0; i < m; i++ {
		for j := 0; j < n; j++ {
			if occupancy[i][j] == 2 {
				countUtil(i, j, m, n, &occupancy)
			}

		}
	}

	count := 0
	for i := 0; i < m; i++ {
		for j := 0; j < n; j++ {
			if occupancy[i][j] == 0 {
				count += 1
			}

		}
	}

	return count
}

func countUtil(x, y, m, n int, occupancy *([][]int)) {

	for i := x + 1; i < m; i++ {
		if (*occupancy)[i][y] == 0 {
			(*occupancy)[i][y] = 1
		}

		if (*occupancy)[i][y] == -1 || (*occupancy)[i][y] == 2 {
			break
		}
	}

	for i := x - 1; i >= 0; i-- {
		if (*occupancy)[i][y] == 0 {
			(*occupancy)[i][y] = 1
		}

		if (*occupancy)[i][y] == -1 || (*occupancy)[i][y] == 2 {
			break
		}
	}

	for i := y + 1; i < n; i++ {
		if (*occupancy)[x][i] == 0 {
			(*occupancy)[x][i] = 1
		}

		if (*occupancy)[x][i] == -1 || (*occupancy)[x][i] == 2 {
			break
		}
	}

	for i := y - 1; i >= 0; i-- {

		if (*occupancy)[x][i] == 0 {
			(*occupancy)[x][i] = 1
		}

		if (*occupancy)[x][i] == -1 || (*occupancy)[x][i] == 2 {
			break
		}
	}

}

func main() {
	output := countUnguarded(4, 6, [][]int{{0, 0}, {1, 1}, {2, 3}}, [][]int{{0, 1}, {2, 2}, {1, 4}})
	fmt.Println(output)

	output = countUnguarded(3, 3, [][]int{{1, 1}}, [][]int{{0, 1}, {1, 0}, {2, 1}, {1, 2}})
	fmt.Println(output)
}

输出

7
4

注意: 请查看我们的 Golang 高级教程。本系列教程内容详细,我们已经尽力通过实例涵盖所有概念。本教程适合那些希望深入理解 Golang 并获得专业知识的人 – Golang 高级教程

如果你有兴趣了解如何在 Golang 中实现所有设计模式。如果你感兴趣,那么这篇文章适合你 – 所有设计模式 Golang

另外,请查看我们的系统设计教程系列 – 系统设计教程系列

恢复二叉搜索树程序

原文:techbyexample.com/recover-binary-search-tree/

概述

给定一个二叉搜索树的根节点。二叉搜索树中有两个节点被交换了。我们需要修复二叉树并恢复其原始结构

程序

以下是相应的程序

package main

import "fmt"

type TreeNode struct {
	Val   int
	Left  *TreeNode
	Right *TreeNode
}

func recoverTree(root *TreeNode) {
	var prev *TreeNode
	var first *TreeNode
	var mid *TreeNode
	var last *TreeNode

	recoverTreeUtil(root, &prev, &first, &mid, &last)

	if first != nil && last != nil {
		first.Val, last.Val = last.Val, first.Val
	} else if first != nil && mid != nil {
		first.Val, mid.Val = mid.Val, first.Val
	}
}

func recoverTreeUtil(root *TreeNode, prev, first, mid, last **TreeNode) {
	if root == nil {
		return
	}

	recoverTreeUtil(root.Left, prev, first, mid, last)

	if *prev == nil {
		*prev = root
	} else if *first == nil && (*prev).Val > root.Val {
		*first = *prev
		*mid = root
	} else if (*prev).Val > root.Val {
		*last = root
	}

	*prev = root

	recoverTreeUtil(root.Right, prev, first, mid, last)
}

func main() {
	root := TreeNode{Val: 2}
	root.Left = &TreeNode{Val: 3}
	root.Right = &TreeNode{Val: 1}

	recoverTree(&root)
	fmt.Println(root.Val)
	fmt.Println(root.Left.Val)
	fmt.Println(root.Right.Val)

}

输出

2
1
3

注意: 查看我们的 Golang 高级教程。本系列教程内容详尽,我们尽力通过实例覆盖所有概念。本教程适合那些希望深入学习并掌握 Golang 的人 – Golang 高级教程

如果你有兴趣了解如何在 Golang 中实现所有设计模式。如果是的话,那么这篇文章适合你 – Golang 中的所有设计模式

另外,查看我们的系统设计教程系列 – 系统设计教程系列

Golang 中的丑数 2 程序

原文:techbyexample.com/program-for-ugly-number-2-in-go-golang/

概述

丑数是指其质因数仅限于 2、3 和 5 的数字。我们已经看到过一个程序,给定一个数字 n,判断它是否是丑数,如果是则返回 true,否则返回 false。以下是该程序的链接 techbyexample.com/program-ugly-number/

在本教程中,我们将编写一个程序,给定一个数字 n,找到第 n 个丑数。

示例 1

Input: 5
Output: 5
Reason: First five ugly numbers are 1, 2, 3, 4, 5 hence 5th ugly number is 5

示例 2

Input: 20
Output: 36

这里的思路是使用动态规划。我们将追踪 2、3 和 5 的倍数。下一个丑数将始终是这三个数中的最小值。

程序

以下是该程序:

package main

import "fmt"

func nthUglyNumber(n int) int {
	dpUgly := make([]int, n)

	dpUgly[0] = 1

	next_multiple_of_2 := 2
	next_multiple_of_3 := 3
	next_multiple_of_5 := 5

	i2 := 0
	i3 := 0
	i5 := 0

	for i := 1; i < n; i++ {
		nextUglyNumber := minOfThree(next_multiple_of_2, next_multiple_of_3, next_multiple_of_5)
		dpUgly[i] = nextUglyNumber

		if nextUglyNumber == next_multiple_of_2 {
			i2++
			next_multiple_of_2 = 2 * dpUgly[i2]
		}

		if nextUglyNumber == next_multiple_of_3 {
			i3++
			next_multiple_of_3 = 3 * dpUgly[i3]
		}

		if nextUglyNumber == next_multiple_of_5 {
			i5++
			next_multiple_of_5 = 5 * dpUgly[i5]
		}

	}

	return dpUgly[n-1]

}

func minOfThree(a, b, c int) int {
	if a < b && a < c {
		return a
	}

	if b < c {
		return b
	}

	return c
}

func main() {
	output := nthUglyNumber(5)
	fmt.Println(output)

	output = nthUglyNumber(20)
	fmt.Println(output)
}

输出

5
36

注意: 请查看我们的 Golang 高级教程。本系列教程内容详尽,我们已尽力通过示例覆盖所有概念。本教程适用于那些希望掌握 Golang 并深入理解其内容的读者 – Golang 高级教程

如果你有兴趣了解如何在 Golang 中实现所有设计模式,那么这篇文章就是为你准备的 – 所有设计模式 Golang

同时,您还可以查看我们的系统设计教程系列 – 系统设计教程系列

丑数的程序

原文:techbyexample.com/program-ugly-number/

概述

丑数是其质因数仅限于 2、3 和 5 的数字。

给定一个数字 n,如果它是丑数,则返回 true,否则返回 false。

示例 1

Input: 12
Output: true

示例 2

Input: 7 
Output: false

思路是

  • 当数字是 2 的因数时,不断将数字除以 2

  • 当数字是 3 的因数时,不断将数字除以 3

  • 当数字是 5 的因数时,不断将数字除以 5

最后,如果数字等于 1,则返回 true,否则返回 false

程序

以下是相应的程序

package main

import "fmt"

func isUgly(n int) bool {
	if n == 0 {
		return false
	}
	if n == 1 {
		return true
	}

	for {
		if n%2 != 0 {
			break
		}
		n = n / 2
	}

	for {
		if n%3 != 0 {
			break
		}
		n = n / 3
	}

	for {
		if n%5 != 0 {
			break
		}
		n = n / 5
	}

	return n == 1
}

func main() {
	output := isUgly(12)
	fmt.Println(output)

	output = isUgly(7)
	fmt.Println(output)
}

输出

true
false

另外,查看我们的系统设计教程系列 – 系统设计教程系列

是否为二分图程序

techbyexample.com/graph-bipartite-program/

概述

给定一个无向图。如果一个图的节点可以被分为两个子集,并且每条边连接一个子集中的节点与另一个子集中的某个节点,那么这个图是二分图。

图中包含 n 个节点,编号从0n-1。输入是一个名为graph的矩阵,这是一个二维矩阵,其中 graph[i]包含第i个节点连接的节点。例如,如果

graph[0] = [1,3]

这意味着节点 0节点 1节点 3相连

示例 1

Input: [[1,3],[0,2],[1,3],[0,2]]
Output: true

示例 2

Input: [[1,4],[0,2],[1,3],[2,4],[0,3]
Output: false

这里的思路是使用深度优先搜索(DFS)。我们将尝试为每个节点分配红色或黑色。如果一个节点被涂上红色,那么它的邻居必须涂上黑色。

  • 如果我们能以这种方式着色,那么图就是二分图

  • 如果在着色过程中,我们发现两个通过边连接的节点有相同的颜色,那么该图不是二分图

让我们来看一下相应的程序

程序

以下是相应的程序

package main

import "fmt"

func isBipartite(graph [][]int) bool {
	nodeMap := make(map[int][]int)

	numNodes := len(graph)

	if numNodes == 1 {
		return true
	}
	for i := 0; i < numNodes; i++ {
		nodes := graph[i]
		for j := 0; j < len(nodes); j++ {
			nodeMap[i] = append(nodeMap[i], nodes[j])
		}
	}

	color := make(map[int]int)

	for i := 0; i < numNodes; i++ {
		if color[i] == 0 {
			color[i] = 1
			isBiPartite := visit(i, nodeMap, &color)
			if !isBiPartite {
				return false
			}
		}
	}

	return true

}

func visit(source int, nodeMap map[int][]int, color *map[int]int) bool {

	for _, neighbour := range nodeMap[source] {
		if (*color)[neighbour] == 0 {
			if (*color)[source] == 1 {
				(*color)[neighbour] = 2
			} else {
				(*color)[neighbour] = 1
			}
			isBipartite := visit(neighbour, nodeMap, color)
			if !isBipartite {
				return false
			}
		} else {
			if (*color)[source] == (*color)[neighbour] {
				return false
			}
		}
	}

	return true
}

func main() {
	output := isBipartite([][]int{{1, 3}, {0, 2}, {1, 3}, {0, 2}})
	fmt.Println(output)

	output = isBipartite([][]int{{1, 4}, {0, 2}, {1, 3}, {2, 4}, {0, 3}})
	fmt.Println(output)

}

输出:

true
false

另外,请查看我们的系统设计教程系列 – 系统设计教程系列

二叉树最大路径和程序

原文:techbyexample.com/binary-tree-maximum-path-sum/

概览

给定一个二叉树,目标是找到该二叉树中的最大路径和。二叉树中的路径是指一系列彼此相连的节点,每个节点在最大路径和中只能出现一次。

示例 1

Output: 16
Maximum Sum Path is: 4->2->1->3->6

示例 2

Output: 14
Maximum Sum Path is: 5->3->6

该方法的思路是在每个节点跟踪以下四个值。

  • a = root.Val

  • b = root.Val + leftSubTreeMaxSum

  • c = root.Val + rightSubTreeMaxSum

  • d = root.Val + leftSubTreeMaxSum + rightSubTreeMaxSum

然后

  • 给定节点的最大路径和是(a,b,c,d)中的最大值。

  • 递归调用中的返回值将是(a,b,c)中的最大值。为什么?这是因为只有 a、b 或 c 的路径才是父节点可以考虑的路径,d 不能考虑,因为它成为了无效路径。通过上面示例二的二叉树来理解这个问题。路径5->3->6不能包括父节点-5,因为它成为了无效路径。

程序

以下是相应的程序代码:

package main

import (
	"fmt"
	"math"
)

type TreeNode struct {
	Val   int
	Left  *TreeNode
	Right *TreeNode
}

func maxPathSum(root *TreeNode) int {
	res := math.MinInt64

	maxPathSumUtil(root, &res)
	return res
}

func maxPathSumUtil(root *TreeNode, res *int) int {
	if root == nil {
		return 0
	}

	l := maxPathSumUtil(root.Left, res)
	r := maxPathSumUtil(root.Right, res)

	a := root.Val
	b := root.Val + l
	c := root.Val + r
	d := root.Val + l + r

	maxReturnSum := maxOfThree(a, b, c)

	maxSumPath := maxOfTwo(maxReturnSum, d)
	if maxSumPath > *res {
		*res = maxSumPath
	}

	return maxReturnSum
}

func maxOfThree(a, b, c int) int {
	if a > b && a > c {
		return a
	}

	if b > c {
		return b
	}

	return c
}

func maxOfTwo(a, b int) int {
	if a > b {
		return a
	}
	return b
}

func main() {
	root := &TreeNode{Val: 1}
	root.Left = &TreeNode{Val: 2}
	root.Left.Left = &TreeNode{Val: 4}
	root.Right = &TreeNode{Val: 3}
	root.Right.Left = &TreeNode{Val: 5}
	root.Right.Right = &TreeNode{Val: 6}

	output := maxPathSum(root)
	fmt.Println(output)

	root = &TreeNode{Val: -10}
	root.Left = &TreeNode{Val: 2}
	root.Left.Left = &TreeNode{Val: 4}
	root.Right = &TreeNode{Val: 3}
	root.Right.Left = &TreeNode{Val: 5}
	root.Right.Right = &TreeNode{Val: 6}
	output = maxPathSum(root)
	fmt.Println(output)

}

输出:

16
14

另外,查看我们的系统设计教程系列 – 系统设计教程系列

按频率排序字符

原文:techbyexample.com/sort-characters-frequency/

概述

给定一个输入字符串。目标是根据字符的频率对字符串进行排序。我们需要根据字符频率的降序进行排序。让我们通过一个示例来理解。

示例 1

Input: "bcabcb"
Output: "bbbcca"

示例 2

Input: "mniff"
Output: "ffmni"

程序

以下是相同程序的代码

package main

import (
	"fmt"
	"sort"
)

func frequencySort(s string) string {
	stringMap := make(map[byte]int)
	lenS := len(s)
	for i := 0; i < lenS; i++ {
		stringMap[s[i]]++
	}

	itemArray := make([]item, 0)

	for key, value := range stringMap {
		i := item{
			char:      key,
			frequency: value,
		}
		itemArray = append(itemArray, i)
	}

	sort.Slice(itemArray, func(i, j int) bool {
		return itemArray[i].frequency > itemArray[j].frequency
	})

	output := ""

	for i := 0; i < len(itemArray); i++ {
		for j := 0; j < itemArray[i].frequency; j++ {
			output = output + string(itemArray[i].char)
		}
	}

	return output

}

type item struct {
	char      byte
	frequency int
}

func main() {
	output := frequencySort("bcabcb")
	fmt.Println(output)

	output = frequencySort("mniff")
	fmt.Println(output)

}

输出:

bbbcca
ffmni

另外,查看我们的系统设计教程系列 – 系统设计教程系列

所得税计算程序

原文:techbyexample.com/program-income-tax-paid/

概述

给定一个二维数组,表示所得税的税率范围。输入数组是brackets,其中

brackets[i] = [upperi, percenti]

这意味着第 i 个税率区间的上限是upperi,并按percent的税率征税。税率区间数组按上限排序。以下是税费的计算方法

  • upper0 以下的部分按 percent0 征税

  • upper1-upper0 按 percent1 征税

  • ……等等

你还将得到收入作为输入。你需要根据这个收入计算所得税。已知最后一个区间的上限大于收入。

示例 1

Input: brackets = [[4,10],[9,20],[12,30]], income = 10
Output: 1.7

示例 2

Input: brackets = [[3,10]], income = 1
Output: 0.3

程序

以下是相应的程序

package main

import "fmt"

func calculateTax(brackets [][]int, income int) float64 {
	if income == 0 {
		return 0
	}

	var totalTax float64

	numBrackets := len(brackets)
	upper := 0
	for i := 0; i < numBrackets; i++ {

		if i == 0 {
			upper = brackets[i][0]
		} else {
			upper = brackets[i][0] - brackets[i-1][0]
		}

		taxPer := brackets[i][1]
		if income <= upper {
			totalTax += float64(income) * float64(taxPer) / 100
			break
		} else {
			totalTax += float64(upper) * float64(taxPer) / 100
			income = income - upper
		}
	}

	return totalTax
}

func main() {
	output := calculateTax([][]int{{4, 10}, {9, 20}, {12, 30}}, 10)
	fmt.Println(output)

	output = calculateTax([][]int{{3, 10}}, 10)
	fmt.Println(output)

}

输出:

1.7
0.3

此外,查看我们的系统设计教程系列:系统设计教程系列

检查 N 及其两倍是否存在

原文:techbyexample.com/number-double-exists/

概述

给定一个数组。目标是查找是否存在一个数字,其两倍也存在于数组中。

示例 1

Input: [8,5,4,3]
Output: true
Explanation: 4 and 8

示例 2

Input: [1,3,7,9]
Output: false
Explanation: There exists no number for which its double exist

这里的思路是使用一个映射。对于每个元素,我们将检查并在满足条件时返回 true。

  • 如果其两倍存在于映射中。

  • 如果数字是偶数,那么检查其一半是否存在于映射中。

程序

以下是实现该功能的程序

package main

import "fmt"

func checkIfExist(arr []int) bool {
	numMap := make(map[int]bool)

	for i := 0; i < len(arr); i++ {
		if numMap[arr[i]*2] {
			return true
		}
		if arr[i]%2 == 0 && numMap[arr[i]/2] {
			return true
		}
		numMap[arr[i]] = true
	}
	return false
}

func main() {
	output := checkIfExist([]int{8, 5, 4, 3})
	fmt.Println(output)

	output = checkIfExist([]int{1, 3, 7, 9})
	fmt.Println(output)

}

输出:

true
false

另外,请查看我们的系统设计教程系列:系统设计教程系列

帕斯卡三角形程序

原文:techbyexample.com/program-for-pascals-triangle/

概述

目标是打印帕斯卡三角形的 n 行,n 的值作为输入传递给程序

示例 1

Input: numRows = 4
Output: [[1],[1,1],[1,2,1],[1,3,3,1]]

示例 2

Input: numRows = 5
Output: [[1],[1,1],[1,2,1],[1,3,3,1],[1,4,6,4,1]]

请参阅此链接了解更多关于帕斯卡三角形的信息:en.wikipedia.org/wiki/Pascal%27s_triangle

这里的思路是使用动态规划。

程序

以下是相应的程序

package main

import "fmt"

func generate(numRows int) [][]int {
	firstRow := []int{1}

	if numRows == 1 {
		return [][]int{firstRow}
	}

	secondRow := []int{1, 1}

	if numRows == 2 {
		return [][]int{firstRow, secondRow}
	}

	output := [][]int{firstRow, secondRow}

	for i := 2; i < numRows; i++ {
		temp := make([]int, i+1)

		lastRow := output[i-1]

		temp[0] = 1
		temp[i] = 1

		for j := 1; j < i; j++ {
			temp[j] = lastRow[j-1] + lastRow[j]
		}

		output = append(output, temp)

	}

	return output

}

func main() {
	output := generate(4)
	fmt.Println(output)

	output = generate(5)
	fmt.Println(output)
}

输出:

[1] [1 1] [1 2 1] [1 3 3 1]]
[[1] [1 1] [1 2 1] [1 3 3 1] [1 4 6 4 1]]

另外,请查看我们的系统设计教程系列:系统设计教程系列

最长连续序列程序

原文:techbyexample.com/longest-consecutive-sequence/

概述

给定一个整数数组,找出其中最长连续序列的长度。让我们通过一个例子来理解这个问题

示例 1

Input: [4,7,3,8,2,1]
Output: 4
Reason: The longest consecutive sequence is [1,2,3,4]

示例 2

Input: [4,7,3,8,2,1,9,24,10,11]
Output: 5
Reason: The longest consecutive sequence is [7,8,9,10,11]

朴素的思路是对数组进行排序,然后返回最长的连续序列。但我们可以在 O(n) 时间内完成此操作。思路是使用哈希表。以下是该思路

  • 将每个数字存储在哈希表中。

  • 然后从每个数字开始,找出以该数字为起点的最长连续序列的长度。如果一个数字假设为 n,我们尝试在哈希表中找到 n+1、n+2 …… 并得到从该数字开始的最长连续序列的长度

  • 如果在步骤 2 中找到的长度大于之前找到的最长连续序列的长度,则更新最长连续序列的长度

程序

以下是相同的程序

package main

import "fmt"

func longestConsecutive(nums []int) int {
	numsMap := make(map[int]int)

	lenNums := len(nums)

	for i := 0; i < lenNums; i++ {
		numsMap[nums[i]] = 0
	}

	maxLCS := 0
	for i := 0; i < lenNums; i++ {
		currentLen := 1
		counter := 1

		for {
			val, ok := numsMap[nums[i]+counter]
			if ok {
				if val != 0 {
					currentLen += val
					break
				} else {
					currentLen += 1
					counter += 1
				}
			} else {
				break
			}
		}

		if currentLen > maxLCS {
			maxLCS = currentLen
		}
		numsMap[nums[i]] = currentLen
	}

	return maxLCS
}

func main() {
	output := longestConsecutive([]int{4, 7, 3, 8, 2, 1})
	fmt.Println(output)

	output = longestConsecutive([]int{4, 7, 3, 8, 2, 1, 9, 24, 10, 11})
	fmt.Println(output)

}

输出:

4
5

此外,请查看我们的系统设计教程系列 – 系统设计教程系列

打印所有二叉树路径

原文:techbyexample.com/print-all-binary-tree-paths/

概述

给定一棵树的根节点,目标是打印所有从根节点开始的树路径。

示例

输出:

1->2->4
1->3->5
1->3->6

程序

以下是相同的程序

package main

import (
	"fmt"
	"strconv"
)

type TreeNode struct {
	Val   int
	Left  *TreeNode
	Right *TreeNode
}

func binaryTreePaths(root *TreeNode) []string {
	output := make([]string, 0)

	binaryTreePathsUtil(root, "", &output)
	return output
}

func binaryTreePathsUtil(root *TreeNode, curr string, output *[]string) {
	if root == nil {
		return
	}

	valString := strconv.Itoa(root.Val)
	if curr == "" {
		curr = valString
	} else {
		curr = curr + "->" + valString
	}
	if root.Left == nil && root.Right == nil {
		*output = append(*output, curr)
		return
	}

	binaryTreePathsUtil(root.Left, curr, output)
	binaryTreePathsUtil(root.Right, curr, output)

}

func main() {
	root := TreeNode{Val: 1}
	root.Left = &TreeNode{Val: 2}
	root.Left.Left = &TreeNode{Val: 4}
	root.Right = &TreeNode{Val: 3}
	root.Right.Left = &TreeNode{Val: 5}
	root.Right.Right = &TreeNode{Val: 6}

	output := binaryTreePaths(&root)
	fmt.Println(output)
}

输出:

[1->2->4 1->3->5 1->3->6]

同时,查看我们的系统设计教程系列 – 系统设计教程系列

程序:反转字符串中的元音字母

原文:techbyexample.com/reverse-vowels-of-a-string/

概述

给定一个字符串。目标是反转该字符串中的所有元音字母

示例 1

Input: "simple"
Output: "sempli"

示例 2

Input: "complex"
Output: "cemplox"

程序

以下是相同的程序

package main

import "fmt"

func reverseVowels(s string) string {
	runeS := []rune(s)
	lenS := len(runeS)

	for i, j := 0, lenS-1; i < j; {
		for i < j {
			if !vowel(runeS[i]) {
				i++
			} else {
				break
			}
		}
		if i == j {
			break
		}
		for i < j {
			if !vowel(runeS[j]) {
				j--
			} else {
				break
			}
		}

		if i == j {
			break
		}

		runeS[i], runeS[j] = runeS[j], runeS[i]
		i++
		j--

	}

	return string(runeS)

}

func vowel(s rune) bool {
	if s == 'a' || s == 'e' || s == 'i' || s == 'o' || s == 'u' {
		return true
	}

	if s == 'A' || s == 'E' || s == 'I' || s == 'O' || s == 'U' {
		return true
	}

	return false
}

func main() {
	output := reverseVowels("simple")
	fmt.Println(output)

	output = reverseVowels("complex")
	fmt.Println(output)

}

输出:

sempli
cemplox

另外,请查看我们的系统设计教程系列 – 系统设计教程系列

判断矩阵是否可以通过旋转获得

原文:techbyexample.com/program-matrix-rotation-target/

概述

给定两个 n*n 矩阵源矩阵目标矩阵。我们需要确定是否可以通过进行任意次数的 90 度旋转将源矩阵转换为目标矩阵

示例 1

Input: source = [2,1],[1,2]], target = [[1,2],[2,1]]
Output: true

源矩阵可以旋转 90 度一次以获得目标矩阵。

示例 2

Input:  source = [[1,2],[2,2]], target = [[2,1],[1,2]]
Output: false

即使我们将源矩阵旋转 90 度 3 次,也无法得到目标矩阵。

程序

以下是相应的程序

package main

import "fmt"

func findRotation(mat [][]int, target [][]int) bool {
	n, tmp := len(mat), make([]bool, 4)
	for i := range tmp {
		tmp[i] = true
	}
	for i := 0; i < n; i++ {
		for j := 0; j < n; j++ {
			if mat[i][j] != target[i][j] {
				tmp[0] = false
			}
			if mat[i][j] != target[j][n-i-1] {
				tmp[1] = false
			}
			if mat[i][j] != target[n-i-1][n-j-1] {
				tmp[2] = false
			}
			if mat[i][j] != target[n-j-1][i] {
				tmp[3] = false
			}
		}
	}
	return tmp[0] || tmp[1] || tmp[2] || tmp[3]
}

func main() {
	output := findRotation([][]int{{2, 1}, {1, 2}}, [][]int{{1, 2}, {2, 1}})
	fmt.Println(output)

	output = findRotation([][]int{{1, 2}, {2, 2}}, [][]int{{2, 1}, {1, 2}})
	fmt.Println(output)

}

输出

true
false

另外,查看我们的系统设计教程系列 – 系统设计教程系列

引爆最大炸弹程序。

原文:techbyexample.com/detonate-maximum-bombs-program/

概述

给定一个二维数组,其中每个数组项有三个值。

  • i – 表示炸弹的 x 坐标。

  • j – 表示炸弹的 y 坐标。

  • r – 表示炸弹的爆炸半径。

一个炸弹在爆炸时会引发其范围内所有炸弹的爆炸。当这些炸弹爆炸时,它们将再次引发其范围内所有炸弹的爆炸。

你只能引爆一个炸弹。这个问题的思路是找出可以引爆的最大炸弹数量。

示例 1

Input: [[1,0,3],[5,0,4]]
Output: 2

第一个炸弹在第二个炸弹的爆炸范围内。所以当我们引爆第二个炸弹时,第二个炸弹和第一个炸弹都会被引爆。

示例 2

Input:  [[2,2,2],[10,10,5]]
Output: 1

这两个炸弹彼此不在对方的爆炸范围内。

解决这个问题的思路是将所有内容视为一个有向图,如果第二个炸弹在第一个炸弹的爆炸范围内,则存在从第一个炸弹指向第二个炸弹的有向边。

一旦构建了这个图,我们可以从每个节点做深度优先搜索(DFS),以获得它能引爆的最大炸弹数。我们还会存储之前计算的结果。

程序

以下是相应的程序。

package main

import (
	"fmt"
	"math"
)

func maximumDetonation(bombs [][]int) int {

	if len(bombs) == 0 {
		return 0
	}
	max := 1
	detonationMap := make(map[int][]int)

	for i := 0; i < len(bombs); i++ {
		for j := 0; j < len(bombs); j++ {
			if i != j {
				if float64(bombs[i][2]) >= distance(bombs[i], bombs[j]) {
					if arr, ok := detonationMap[i]; ok {
						arr = append(arr, j)
						detonationMap[i] = arr
					} else {
						var arr []int
						arr = append(arr, j)
						detonationMap[i] = arr
					}
				}
			}
		}
	}

	for key := range detonationMap {
		detonated := 1
		queue := []int{key}
		visited := make(map[int]bool)
		visited[key] = true

		for len(queue) > 0 {
			cur := queue[0]
			queue = queue[1:]

			for _, val := range detonationMap[cur] {
				if !visited[val] {
					detonated++
					visited[val] = true
					queue = append(queue, val)
				}
			}
		}
		if detonated == len(bombs) {
			return len(bombs)
		}

		if detonated > max {
			max = detonated
		}
	}
	return max
}

func distance(a []int, b []int) float64 {
	ret := math.Sqrt(math.Pow(float64(a[0]-b[0]), 2) + math.Pow(float64(a[1]-b[1]), 2))
	return ret
}

func main() {
	output := maximumDetonation([][]int{{1, 0, 3}, {5, 0, 4}})
	fmt.Println(output)

	output = maximumDetonation([][]int{{2, 2, 2}, {10, 10, 5}})
	fmt.Println(output)

}

输出:

2
1

注意: 查看我们的 Golang 高级教程。本系列教程内容详尽,并且我们尽力通过实例覆盖所有概念。本教程适合那些希望深入掌握并精通 Golang 的人 – Golang 高级教程

如果你有兴趣了解如何在 Golang 中实现所有设计模式。如果是的话,那这篇文章适合你 – 所有设计模式 Golang

同时,也可以查看我们的系统设计教程系列 – 系统设计教程系列

腐烂橙子程序

原文:techbyexample.com/rotting-oranges-program/

概述

给定一个 m*n 的矩阵,每个条目包含三个值。

  • 0 – 表示该条目为空

  • 1 – 表示该条目包含新鲜的橙子

  • 2 – 表示该条目包含腐烂的橙子

一只腐烂的橙子将在 1 天内腐烂相邻的橙子。对于给定的橙子,任何位于其上、下、左、右的橙子都是相邻橙子。对角线上的橙子不算在内。

目标是找到所有橙子腐烂所需的天数。如果所有橙子无法腐烂,则输出 -1。如果一个新鲜橙子无法从腐烂橙子传播到达,就会发生这种情况。

示例 1

Input: [[2,1,1],[1,1,0],[0,1,1]]
Output: 4

顶部有一个腐烂的橙子,它将腐烂其相邻的两个橙子。这些腐烂的相邻橙子会进一步腐烂它们的相邻橙子。

示例 2

Input: [[1,1]]
Output: -1

程序

以下是相同程序的代码

package main

import "fmt"

func orangesRotting(grid [][]int) int {
	numRows := len(grid)
	numColumns := len(grid[0])

	queue := make([][2]int, 0)

	for i := 0; i < numRows; i++ {
		for j := 0; j < numColumns; j++ {
			if grid[i][j] == 2 {
				queue = append(queue, [2]int{i, j})
			}
		}
	}

	neighboursIndex := [][2]int{{1, 0}, {-1, 0}, {0, 1}, {0, -1}}
	numMinutes := 0
	for {
		n := len(queue)
		for i := 0; i < n; i++ {
			pop := queue[0]
			queue = queue[1:]

			a := pop[0]
			b := pop[1]
			for k := 0; k < 4; k++ {
				neighbourX := a + neighboursIndex[k][0]
				neighbourY := b + neighboursIndex[k][1]

				if isValid(neighbourX, neighbourY, numRows, numColumns) {
					if grid[neighbourX][neighbourY] == 1 {
						grid[neighbourX][neighbourY] = 2
						queue = append(queue, [2]int{neighbourX, neighbourY})
					}
				}

			}
		}

		if len(queue) == 0 {
			break
		}
		numMinutes++
	}

	for i := 0; i < numRows; i++ {
		for j := 0; j < numColumns; j++ {
			if grid[i][j] == 1 {
				return -1
			}
		}
	}

	return numMinutes
}

func isValid(i, j, numRows, numColumns int) bool {
	if i >= numRows || i < 0 {
		return false
	}

	if j >= numColumns || j < 0 {
		return false
	}
	return true
}

func main() {
	output := orangesRotting([][]int{{2, 1, 1}, {1, 1, 0}, {0, 1, 1}})
	fmt.Println(output)

	output = orangesRotting([][]int{{1, 1}})
	fmt.Println(output)
}

输出:

4
-1

此外,请查看我们的系统设计教程系列:系统设计教程系列

计算无向图中无法互通的节点对数量

原文:techbyexample.com/count-unreachable-pair-nodes/

概述

给定一个整数 n,表示节点的数量,节点编号从 0 到 n-1。还给定了一个二维整数数组 edges,其中 edges[i] = [ai, bi] 表示从 ai 到 bi 之间存在一条无向边。

目标是找出彼此无法互通的节点对的数量

示例 1

n=3
edges=[{0,1}]

输出

2

我们有两对节点是未连接的

[{0,2}, {1,2}]

示例 2

n=9
edges=[{0,1},{0,4},{0,5},{2,3},{2,6},{7,8}]

输出:

26

我们有 26 对节点是未连接的

[{0,2}, {0,3}, {0,6}, {0,7}, {0,8},
{1,2}, {1,3}, {1,6}, {1,7}, {1,8},
{4,2}, {4,3}, {4,6}, {4,7}, {4,8},
{5,2}, {5,3}, {5,6}, {5,7}, {5,8},
{7,2}, {7,3}, {7,6},
{8,2}, {8,3}, {8,6}]

这个思路是从每个未访问的节点开始进行深度优先搜索(DFS),以识别断开图中每个连接图的节点数量。例如,上图中,每个连接图的节点数量是

4
3
2

然后我们只需找到每个已连接图中节点对的数量

程序

下面是实现此逻辑的程序

package main

import "fmt"

func countPairs(n int, edges [][]int) int64 {
	nodeMap := make(map[int][]int)

	for i := 0; i < len(edges); i++ {
		nodeMap[edges[i][0]] = append(nodeMap[edges[i][0]], edges[i][1])
		nodeMap[edges[i][1]] = append(nodeMap[edges[i][1]], edges[i][0])
	}

	visited := make(map[int]bool)

	var output int64
	var totalNodesVisited int64
	for i := 0; i < n; i++ {
		if !visited[i] {
			nodeVisited := visit(i, nodeMap, &visited)
			if totalNodesVisited != 0 {
				output += totalNodesVisited * nodeVisited
			}
			totalNodesVisited += nodeVisited
		}
	}
	return output
}

func visit(source_node int, nodeMap map[int][]int, visited *map[int]bool) int64 {
	(*visited)[source_node] = true

	var totalNodeVisited int64
	totalNodeVisited = 1
	neighbours, ok := nodeMap[source_node]
	if ok {
		for _, neighbour := range neighbours {
			if !(*visited)[neighbour] {
				nodeVisited := visit(neighbour, nodeMap, visited)
				totalNodeVisited += nodeVisited
			}
		}
	}

	return totalNodeVisited
}

func main() {

	n := 3
	edges := [][]int{{0, 1}}
	output := countPairs(n, edges)
	fmt.Println(output)

	n = 9
	edges = [][]int{{0, 1}, {0, 4}, {0, 5}, {2, 3}, {2, 6}, {7, 8}}
	output = countPairs(n, edges)
	fmt.Println(output)
}

输出:

2
26

注意: 查看我们的 Golang 高级教程。本系列教程内容详尽,涵盖了所有概念,并附有示例。这篇教程适合那些希望深入学习 Golang,提升专业技能并获得扎实理解的读者 – Golang 高级教程

如果你有兴趣了解如何在 Golang 中实现所有设计模式。如果是的话,那么这篇文章适合你 – 所有设计模式 Golang

同时,也可以查看我们的系统设计教程系列 – 系统设计教程系列

在数组中找到只出现一次的数字

原文:techbyexample.com/number-appear-array/

概述

给定一个数组,其中每个元素出现两次,除了一个元素。目标是找到那个只出现一次的元素,并且在常数额外空间中完成。

示例 1

Input: [2, 1, 2, 3, 3]
Output: 1

示例 2

Input: [1, 1, 4]
Output: 4

这个方法是使用异或运算。我们将利用异或的两个性质。

  • 一个数字与其自身进行异或运算结果为 0

  • 0 与任何数字进行异或运算,结果是该数字

所以,方法是对数组中的所有数字进行异或运算。最后得到的数字就是答案。

程序

以下是对应的程序

package main

import "fmt"

func singleNumber(nums []int) int {
	lenNums := len(nums)
	res := 0
	for i := 0; i < lenNums; i++ {
		res = res ^ nums[i]
	}

	return res
}

func main() {
	output := singleNumber([]int{2, 1, 2, 3, 3})
	fmt.Println(output)

	output = singleNumber([]int{1, 1, 4})
	fmt.Println(output)

}

输出:

1
4

同时,查看我们的系统设计教程系列:系统设计教程系列

数组中的多数元素

原文:techbyexample.com/majority-element-array/

概述

目标是找出给定数组中的多数元素。多数元素是指在给定数组中出现次数超过 n/2 的元素,其中 n 是数组的长度

示例 1

Input: [2, 1, 2, 2, 3]
Output: 2

示例 2

Input: [1]
Output: 1

程序

以下是相应的程序

package main

import "fmt"

func majorityElement(nums []int) int {
	lenNums := len(nums)

	if lenNums == 1 {
		return nums[0]
	}

	numsMap := make(map[int]int)

	for i := 0; i < lenNums; i++ {
		_, ok := numsMap[nums[i]]
		if ok {
			numsMap[nums[i]] = numsMap[nums[i]] + 1
			if numsMap[nums[i]] > lenNums/2 {
				return nums[i]
			}
		} else {
			numsMap[nums[i]] = 1
		}
	}

	return 0

}

func main() {
	output := majorityElement([]int{2, 1, 2, 2, 3})
	fmt.Println(output)

	output = majorityElement([]int{1})
	fmt.Println(output)
}

输出:

2
1

同时,查看我们的系统设计教程系列:系统设计教程系列

唯一 ID 生成系统设计

原文:techbyexample.com/unique-id-generation/

目录

  • 概述

  • 唯一 ID 的类型

    • 唯一 ID(松散可排序)")

    • 唯一 ID(不可时间排序)或随机 UUID or Random UUID")

    • 序列号

  • 唯一 ID(松散可排序)")

    • UUID 或 GUID

    • 96 位 GUID

    • 64 位 ID

    • Twitter 的 Snowflake

    • Instagram ID 生成器

  • 唯一 ID(不可时间排序)或随机 UUID or Random UUID")

    • 6 位、7 位或 8 位长度的 Base64 编码数字

    • 16 位或 32 位随机 ID

  • 序列号

  • 时间可排序唯一 ID 的高层设计

    • API 详情

    • 时间组件

    • 高层设计

  • 不可时间排序的唯一 ID(或随机 UUID)的高层设计 or Random UUID")

    • 数据库架构

    • 选择使用哪个数据库

    • 如何解决并发问题

    • 如果键范围耗尽会发生什么

    • KGS 服务不是单点故障吗?

  • 结论

概述

分布式系统如今已成为常态。有时需要在这些分布式系统中生成唯一 ID。这些唯一 ID 的需求可能包括:

  • 公司中有一个订单管理系统,每个订单都需要有一个唯一的 ID。

  • 系统中存储的每个用户都需要有一个唯一 ID。

  • 如果我们以 Instagram 为例,那么用户可能会有数十亿条状态更新或帖子。每个帖子都需要一个不同的唯一 ID。

  • ……

唯一 ID 的类型

此外,在唯一 ID 生成器方面,有三种类型的 ID 可以生成

  • 唯一 ID(松散可排序)

  • 唯一 ID(非松散可排序)或随机唯一 ID

  • 序列号

首先让我们看看这些定义,然后再详细了解每个唯一 ID

唯一 ID(松散可排序)

这些 ID 是松散的时间可排序,并且可以以去中心化的方式生成。正如我们在本文中将看到的那样,有多种生成方法。这些 ID 通常包含时间组件,通常是纪元时间。

示例包括 UUID(128 位),MongoDB 对象 ID(96 位),64 位 ID。由于这些 ID 是时间可排序的,因此可以被应用程序用于组织数据。

UUID 示例(128 位)

815b15597e8a4a4d4302d73e4682f4fc
442bc58166b6ab626ceed57f51982474
442bc58166b6ab626ceed57f51982474

唯一 ID(不可时间排序)或随机 UUID

这些通常是短 ID,通常长度为 6、7 或 8 个字符。例如,有一个短字符串用于生成短网址。这些短网址可以被像 bitly.com 或 YouTube 这样的网站使用。YouTube 使用这些短网址来分享视频,方便分享。因此,这些 ID 本身并没有排序的用例。它们也可以是大的 ID,且与排序无关。

一个大随机 ID 的示例是,当某人在支付网关进行支付时,提供的参考 ID 或交易 ID。这些 ID 完全随机,避免了任何人能够猜测参考 ID 或交易 ID。

可能还会有更多类似的需求,我们需要生成数十亿条记录。

7 个字符 ID 的示例

ETL2W7q
aTyUW89
bSZVK7I

序列号

正如其名字所示,这是一个自动递增的数字。在分布式系统中生成序列号时,需要了解系统中其他工作者的信息。因此,它需要一个共享状态。由于需要集中管理,在分布式系统中高效地生成序列号是非常困难的。

一个序列号的示例是 MySQL 自动递增数字

示例

1
2
3
.
.
.
n

我们首先来看一下描述和设计

  • 唯一 ID(松散可排序)

  • 接下来是唯一 ID(非松散可排序)或随机唯一 ID

  • 最后,我们将讨论序列号生成

唯一 ID(松散可排序)

一些唯一 ID 生成系统的需求可能是:

  • 生成的唯一 ID 应当是短的

  • 生成的 ID 应当是时间可排序的

  • 应该能够以可用的方式每秒生成 10000 个 ID

我们系统中可能生成的一些唯一 ID 是:

  • UUID 或 GUID,长度为 128 位

  • 96 位 UUID。例如 Mongo DB 对象 ID

  • 64 位 UUID

  • 56 位 UUID

UUID 或 GUID

UUID 代表通用唯一标识符。它是一个 16 字节或 128 位的 ID,保证唯一。

UUID 或 GUID 的优势

  • 不需要专门的独立系统来生成这些 ID

  • 这些 ID 的生成速度很快

  • 这些 ID 是按时间排序的。

缺点

  • GUID 或 UUID 的主要缺点是其大小。

  • 可能会发生冲突,但几率非常小。

96 位 GUID

Mongo DB 对象为 12 字节或 96 位。每个 ID 包含:

  • 一个 4 字节的时间戳值,表示从纪元时间以来的秒数。

  • 一个 5 字节的随机值。

  • 一个 3 字节的增量计数器。

该对象 ID 在 Mongo DB 世界中对于每个分片都是唯一的。这意味着,由两个不同分片生成的两个不同对象 ID 可能是相同的。

优点

  • 生成的 ID 是时间可排序的。

缺点

  • 尺寸仍然较大,这可能导致索引变得庞大。

  • 因此,这些 ID 不能在需要小于 64 位的 ID 的地方使用。

64 位 ID

可以生成 64 位的 ID,这些 ID 是按时间排序的。而且由于它是 64 位,它的大小显然也较小。

对于 64 位 ID,我们有两种方法需要考虑。

Twitter 的 Snowflake

Twitter 的 Snowflake 被创建用于生成 64 位唯一 ID。它使用三个标准来确定唯一 ID。

  • 当前时间戳(毫秒级)——为 41 位,基于纪元系统,它给我们 69 年的时间。

  • 生成唯一 ID 的实例的序列号——为 10 位,因此允许 2¹⁰ = 1024 个实例。

  • 生成唯一 ID 的实例中的线程的序列号——为 12 位,允许每个实例 2¹² = 4096。

  • 1 位保留供未来使用。

让我们看看这个系统的可扩展性。可以有 1024 个实例,因此没有单点故障。通过 1024 个实例,系统现在有更多节点来处理流量。

缺点

  • 它需要 Zookeeper 来管理机器的映射。

  • 需要多个 Snowflake 服务器,这增加了管理和复杂度。

Instagram ID 生成器

它还生成一个 64 位 ID。每个 ID 包含:

  • 41 位时间戳(毫秒级)。如果我们有自定义纪元,它可以提供 69 年的时间。

  • 13 位代表逻辑分片 ID。

  • 10 位表示每个分片的序列号。

基本上,每个分片每毫秒可以生成 1024 个 ID。

分片系统有成千上万的逻辑分片,这些分片映射到少数物理机器。由于它使用逻辑分片,因此我们可以从较少的物理服务器开始,这些服务器包含许多逻辑分片。一旦分片变大,我们可以添加更多物理服务器并将这些分片迁移到新物理服务器中。只需要将分片移到新的物理服务器,无需进行数据的重新分配。

Instagram ID 生成器不需要像 Twitter Snowflake 那样的 Zookeeper。它使用 Postgress 架构特性进行管理。

什么是 Postgress 架构

Postgress 数据库由多个模式(schema)组成,每个模式又包含多个表。表名必须在每个模式内唯一。所以在 Instagram 的例子中,每个逻辑分片会映射到一个模式,模式中的表用来生成 ID。

唯一 ID(不可按时间排序)或随机 UUID

这种 ID 的示例。

  • 6、7 或 8 位长度,分别为 36、42 和 48 位长度。

  • 16 字符长度的 ID。

  • 32 字符长度的 ID。

6 或 7 或 8 位的 base 64 编码数字

由于它是 base 64 编码的,因此每个数字占 6 位。

  • 所以如果唯一 ID 是 6 位,那么总长度将是 7*6=42 位。

  • 所以如果唯一 ID 是 7 位,那么总长度将是 7*6=42 位。

  • 如果唯一 ID 的大小是 8,那么总长度将是 8*6=48 位。

使用此类 ID 的一些用例是:

  • URL 短链接系统,其中要求 URL 尽可能短。

  • Paste Bin 类型的系统再次要求生成的 Paste Bin 需要短小。

  • Youtube 需要这样的 ID 来生成视频的短链接。

优点

  • 大小非常小,因此适合共享。

缺点

  • 这些生成的 ID 不是按时间排序的。

16 或 32 位随机 ID

这种 ID 的字符范围可以超过 100 个 ASCII 字符。由于它使用了一个大的字符集,因此生成这些 ID 非常简单,碰撞的几率也较小。

16 位随机 ID 示例。

PO2bc58166b6ab62
E#5B15597e8a$a4$

32 位随机 ID 示例。

PO2bc58166b6ab626ceed57f5198247$
E#5B15597e8a$a4$YU02d73e4682f4FC

序列号

这些 ID 可以通过使用一个数据库来生成,数据库可以为我们提供自动递增的数字。由于是数据库生成唯一 ID,因此可以保证 ID 的唯一性。

顺序生成的唯一 ID。例如,数据库生成唯一递增的 ID。

优点

  • 可以生成短增量 ID。

  • 它易于按时间排序。

缺点

  • 生成的唯一 ID 可以是任意长度。

  • 系统不可扩展,生成唯一 ID 的实例是单点故障。

为了提高可扩展性并防止单点故障,我们可以增加实例的数量。每个实例的递增方式会不同。

  • 例如,如果我们有两个实例,那么一个实例会生成偶数,另一个实例会生成奇数。

  • 如果我们有三个实例,第一个实例会生成 3 的倍数的 ID,第二个实例会生成 3 的倍数加 1 的 ID,第三个实例会生成 3 的倍数加 2 的 ID。

即使增加实例的数量,仍然有一些缺点。

  • 它不再是一个序列号。

  • 由两个不同实例生成的唯一 ID 不再按时间排序。例如,假设有两个实例,一个实例可能生成类似 1001 或 1003 的 ID,另一个实例可能会同时生成类似 502 或 506 的 ID。显然,仅通过查看两个 ID 很难判断哪个先生成。

  • 如果流量非常大,我们必须增加更多的实例。类似地,假设流量较少,我们需要减少一些实例。增加和减少实例可能涉及更改每个实例的 ID 生成逻辑,管理这些事情既复杂又困难。

Flickr 唯一 ID 生成器采用了上述方法。

可排序的时间唯一 ID 高级设计

系统的部分非功能性要求是它应该具有高度的可扩展性和可用性

API 详细信息

我们的系统中只需要一个 API。这个 API 将用于获取一批生成的唯一 ID。因此,它基本上会返回一个唯一 ID 数组。

我们将生成一个包含以下字段的 ID

  • 时间部分

  • 机器编号

  • 进程或线程编号

  • 本地计数器

这是每个字段的描述

  • 时间部分 – 它表示唯一 ID 的时间组件。将时间组件添加到唯一 ID 中,使其可以按时间排序。

  • 机器编号 – 这是机器、实例或容器的唯一编号。

  • 线程编号 – 分配给每个线程的唯一编号

  • 本地计数器 – 这是线程在一毫秒内可以生成的唯一 ID 数量

我们将生成一个 64 位的 ID。通过 64 位 ID,我们可以生成 2⁶⁴ 个 ID。如果我们的需求是每秒 1 亿个 ID,

时间组件

时间组件所占的 64 位数将取决于我们应用程序的生命周期。时间戳将是从 EPOCH 时间开始的毫秒数。还要注意,EPOCH 时间戳从 1970 年 1 月 1 日开始。但对于我们的应用程序,可以有一个自定义的纪元时间戳,开始时间是 2022 年 1 月 1 日或其他任何与应用程序开始相关的日期。

假设我们的应用程序生命周期是 50 年。我们的唯一 ID 将包含毫秒组件。因此,50 年的毫秒数为

50365246060 = 1577000000000 毫秒

存储如此大数字所需的位数是 41 位。基本上,41 位是因为

2⁴⁰ < 1577000000000 < 2⁴¹

实际上,这里有一个表格,告诉你时间戳的位数如何定义年份。

位数 最大二进制数 毫秒数 年份数
40 位 1111111111111111111111111111111111111111 1099511627979 34.8 年
41 位 11111111111111111111111111111111111111111 2199023255755 69.7 年
42 111111111111111111111111111111111111111111 4398046511307 139.4 年

让我们看看每个组件需要多少位。

  • 时间组件 – 41 位

  • 机器组件 – 10 位 – 最大 2¹⁰ = 1024 个实例或机器或容器

  • 线程组件 – 3。每个实例或机器或容器最多 2³ 个线程。

  • 本地计数器 – 10。每个容器每毫秒最多生成 2¹⁰ 个唯一 ID。

让我们检查一下我们服务的容量

  • 最大实例数 – 2¹⁰ = 1024

  • 每个实例的线程数 – 2³ = 8

  • 最大线程数 – 2¹⁰*2³ = 2¹³ = 8192

  • 每个线程每毫秒生成的唯一 ID = 2¹⁰ = 1024

  • 每毫秒可能生成的唯一 ID 数量 = 2¹³*2¹⁰ = 2²³ = 8,388,608 = 每毫秒大约 800 万个

  • 每秒可能生成的唯一 ID 数量 = 2²³*1000 = 8,388,608,000 = 每秒 80 亿个唯一 ID

所以理论上这个系统每秒可以生成 80 亿个唯一 ID,总时长为 69.7 年……我们也可以拥有一个生成 56 位 ID 的系统。让我们看看每个组件需要多少位

  • 时间组件 – 41 位

  • 机器组件 – 8 位 – 最大 2⁸ = 256 个实例、机器或容器

  • 线程组件 – 2 位。每个实例、机器或容器最多可有 2² = 4 个线程

  • 本地计数器 - 4 位。每毫秒每个容器最多可生成 2⁴ 个唯一 ID。

让我们检查一下服务的容量

  • 最大实例数 – 2¹⁰ = 1024

  • 每个实例的线程数量 – 2² = 4

  • 最大线程数 – 2¹⁰*2² = 2¹² = 4096

  • 每个线程每毫秒生成的唯一 ID(本地计数器) = 2⁴ = 16

  • 每毫秒可能生成的唯一 ID 数量 = 2¹²*2⁴ = 2¹⁶ = 65,536 = 每毫秒大约 65K

  • 每秒可能生成的唯一 ID 数量 = 2¹⁶*1000 = 65,536,000 = 每秒 6500 万个唯一 ID

所以理论上这个系统每秒可以生成 6500 万个唯一 ID,总时长为 69.7 年

我们能选择一个非常长的时间戳,使得时间戳可以达到 10000 年或更长吗?

选择一个非常长的时间戳将需要更多的位数,这将限制工作节点、线程或本地计数器的数量。这反过来又限制了每秒生成唯一 ID 的能力。

我们如何为系统中的每个机器或实例分配一个唯一编号?

我们可以在这里使用 ZooKeeper。每当一个新的实例、机器或容器启动时,它可以向 ZooKeeper 注册并从 ZooKeeper 获取一个唯一编号

高层设计

  • 将会有一个 ZooKeeper 服务

  • 将会有一个负载均衡器,后面会有多个实例

  • 每个实例启动时,它会向 ZooKeeper 注册。当它注册时,它会从 ZooKeeper 获得一个唯一的编号。这个唯一编号的范围从 1 到 1024,因为我们只有 10 位用于实例编号。

  • 实例的自动扩展将基于 CPU 负载进行。每个集群中的节点都会向一个监控者发送其 CPU 状态。监控者将根据 CPU 负载来添加或移除集群中的实例或机器。如果我们使用 AWS,那么设置自动扩展非常简单

以下是相应的高层设计图

唯一 ID 的高层设计(非时间可排序)或随机 UUID

对此,我们只关注 6、7 和 8 位长度的随机 UUID 的设计。生成 16 位或 32 位长度的随机 UUID 是直接的,因为没有碰撞的机会。

以下是系统中生成 6、7 和 8 字符长度随机 UUID 的高层组件:

  • 密钥生成服务 – 该服务负责生成短密钥

  • 上游服务 – 这是一个业务服务,使用所有生成的短密钥进行业务用途。

  • 密钥恢复服务 – 该服务将是一个工作进程,负责恢复过期的密钥,并将其重新放入数据库供将来使用。

  • Kafka/SNS+队列/SQS 系统

  • 数据库

  • 缓存

因此,密钥生成服务(KGS 服务)将负责生成短密钥。首先,我们来看看每个密钥的长度应该是多少。可选的长度有 6、7、8。生成密钥时只能使用 base64 URL 安全字符。以下是 URL 安全字符:

  • 小写字母 – “a-z”

  • 大写字母 – “A-Z”

  • 数字 – “0-9”

  • 下划线 – “_”

  • 连字符 – “-”

由于只能使用 URL 安全字符,因此:

  • 对于 6 个字符 – 我们有 64⁶ = 687 亿个选项。

  • 对于 7 个字符 – 我们有 64⁷ = ~3500 亿个选项。

  • 对于 8 个字符 – 我们有 64⁸ = 万亿个选项。

现在我们可以假设 687 亿条记录已经足够,所以我们可以将密钥的字符长度定为 6。现在的问题是这些记录将如何在数据库中维护。如果我们在数据库中存储 687 亿条记录,那么可能会有太多条记录,浪费资源。

一种选择是将一组密钥范围存储在数据库中。我们可以存储一个 64 范围,只保存前五个字符。这五个字符将作为所有 64 个密钥的前缀,这些密钥可以通过该前缀生成。假设我们有以下前缀:

adcA2

然后可以从此生成以下 64 个密钥:

  • adcA2[a-z] – 26 个密钥

  • adcA2[A-Z] – 26 个密钥

  • adcA2[0-9] – 10 个密钥

  • adcA2[-_] – 2 个键

我们可以将这些范围存储在数据库中。所以对于 6 个字符,我们将在数据库中有总共 64⁵ 条记录。

密钥将由密钥服务按范围和批次返回给 Tiny URL 服务。上游服务随后将使用此前缀生成 64 个密钥,并处理 64 个不同的创建短网址请求。

这是优化,因为上游服务只有在用完所有 64 个键时,才需要调用密钥生成服务。所以,上游服务将只调用密钥生成服务一次,以生成 64 个短网址。

现在我们来看一下 KGS 服务的要点。

  • 数据库架构

  • 使用哪个数据库?

  • 如何解决并发问题?

  • 如何恢复 key_prefix

  • 如果密钥范围耗尽会发生什么?

  • 如果短网址永不过期怎么办?

  • KGS 服务不是单点故障吗?

数据库架构

只会有一个表来存储密钥范围,即前缀。以下是表中的字段:

  • key_prefix

  • key_length – 目前将始终为 6。如果在某些情况下需要 7 个字符的密钥,则会有这些字段。

  • used – 如果为真,则说明该密钥前缀当前正在使用。如果为假,则表示该前缀可以自由使用。

  • 已创建

  • 已更新

选择使用哪种数据库

我们没有任何 ACID 要求,因此可以使用 No SQL 数据库。此外,我们可能还需要保存非常大的数据,因此 No SQL 可能更适合。这个系统将是一个写重型和读重型系统。所以我们可以在这里使用Cassandra 数据库

我们可以对数据库进行容量估算,基于此我们可以决定要拥有的分片数量。每个分片也将进行适当的复制。

我们可以在这里做进一步优化来提高延迟。我们可以重新填充缓存中的空闲键范围,KGS 服务可以直接从那里获取,而不是每次都去访问数据库。

如何解决并发问题

很有可能会发生两个请求看到相同的前缀或范围为空。由于多个服务器同时从键数据库读取,我们可能会遇到这种情况,即两个或更多的服务器从键数据库读取相同的键为空。

解决我们刚刚提到的并发问题有两种方法

  • 两个或多个服务器读取相同的键,但只有一个服务器能够在数据库中将该key_prefix标记为已使用。并发发生在数据库级别,即每行在更新前被锁定,我们可以利用这一点。数据库将返回是否有记录被更新。如果记录未更新,服务器可以获取一个新键。如果记录已更新,则该服务器获得正确的键。

  • 另一个选项是使用一个事务,在一个事务中同时进行查找和更新。每次查找和更新都会返回一个唯一的 key_prefix。这可能不是一个推荐的选项,因为它会给数据库带来很大的负担。

如果键范围耗尽会怎样

这将是一个意外的情况。将有一个后台工作者检查键范围是否耗尽。如果是的话,它可以生成 7 长度键的范围。但是它如何知道键范围是否耗尽呢?为了保持大致的计数,可以有另一个表来存储已使用键的用户计数。

  • 每当 KGS 将任何范围分配给上游服务时,它将发布一条消息,消息将被一个同步工作者接收,后者将减少已用键的计数值。

  • 同样,每当一个范围为空时,我们可以递增这个计数器。

KGS 服务不是单点故障吗?

为了防止这种情况发生,我们将对键数据库进行适当的复制。此外,服务本身将有多个应用服务器。我们还将设置适当的自动扩展。我们还可以进行灾难管理。

以下是整个系统的高层次图示

结论

以上是关于唯一 ID 生成的内容,希望你喜欢这篇文章。请在评论中分享你的反馈。

排序数组中的二分查找程序

原文:techbyexample.com/program-binary-search/

概述

思路是对给定的目标元素在输入数组中进行二分查找。如果目标元素存在,则输出其索引。如果目标元素不存在,则输出 -1。

预期时间复杂度为 O(logn)

示例 1

Input: [1, 4, 5, 6]
Target Element: 4
Output: 1

Target element 4 is present at index 1

示例 2

Input: [1, 2, 3]
Target Element: 4
Output: -1

Target element 4 is present at index 1

程序

package main

import "fmt"

func search(nums []int, target int) int {
	return binarySearch(nums, 0, len(nums)-1, target)
}

func binarySearch(nums []int, start, end, target int) int {
	if start > end {
		return -1
	}
	mid := (start + end) / 2

	if nums[mid] == target {
		return mid
	}

	if target < nums[mid] {
		return binarySearch(nums, start, mid-1, target)
	} else {
		return binarySearch(nums, mid+1, end, target)
	}
}

func main() {
	index := search([]int{1, 4, 5, 6}, 4)
	fmt.Println(index)

	index = search([]int{1, 2, 3, 6}, 4)
	fmt.Println(index)
}

输出

1
-1

另请查看我们的系统设计教程系列 – 系统设计教程系列

Pastebin 系统设计

原文:techbyexample.com/pastebin-system-design/

目录

  • 概览

  • 功能性需求

  • 非功能性需求

  • 容量估算

    • 流量估算

    • 数据估算

    • 文本上传

    • 图片上传

  • 数据库架构

    • 文本存储

    • 图片存储

    • 用户表

    • Paste 表

  • 如何生成唯一的 URL

  • 高级设计

  • Paste Bin 服务

    • 上传

    • 下载

  • 密钥生成服务

    • 数据库架构

    • 使用哪个数据库

    • 如何解决并发问题

    • 如何恢复 key_prefix

    • 如果密钥范围耗尽会发生什么

    • 如果 PasteBin 永不过期会怎样

    • KGS 服务不是单点故障吗?

  • 其他常见组件

  • 非功能性需求

    • 可扩展性

    • 低延迟

    • 可用性

    • 告警与监控

    • 向用户位置靠近

    • 避免单点故障

  • 结论

概览

首先,让我们来看一下Pastebin的定义。Pastebin 服务允许你通过链接共享文本和图片,这些链接可以与多个用户共享。所以,基本上Pastebin是一种允许你通过链接暂时共享数据的服务。

需求分析是任何系统设计问题中的关键部分。它分为两个部分。

  • 功能需求 – 定义业务需求

  • 非功能需求 – 定义系统的质量属性,如性能、可扩展性、安全性等。

让我们来看一下 Pastebin 服务的一些功能性和非功能性需求。

功能需求

  • 用户应该能够粘贴文本或上传图像,并通过粘贴本服务生成的唯一 URL 与其他人共享该文本或图像

  • 用户应该能够为 URL 设置过期时间。如果没有指定,默认过期时间为 1 周

  • 用户可以是登录状态或匿名状态

  • 用户应该能够登录并查看自己生成的粘贴本

  • 其他用户可以在访问粘贴 URL 时访问粘贴的文本内容或图像。

非功能需求

  • 系统应该具有高持久性。一旦生成的唯一 URL 应该能够持续存在。

  • 系统应该具备强一致性。这意味着一旦生成粘贴本,系统应该能够在下一个即时请求中返回该粘贴本。

  • 系统应该具有高可用性

  • 系统应该具备容错能力

  • 系统应该没有单点故障

容量估算

我们将对三项内容进行容量估算

  • 网络估算

  • 数据估算

  • 流量估算

流量估算

  • 假设每日活跃用户数为 200K

  • 创建的粘贴本数量 – 200K

  • 每个创建的粘贴本被读取 10 次。总读取量 = 200K*10 = 2000K

所以我们的系统比写操作更偏向读取操作 —— 所以每秒写入请求总数 = 200K/246060 ~ 3 请求/秒。考虑到峰值流量 = 200 请求/秒。每秒读取请求总数 = 2000K/246060 – 30 请求/秒。考虑到峰值流量 = 2000 请求/秒

数据估算

  • 在创建 Pastebin 时,用户可以指定文本或图像。假设用户允许的最大文本大小为 5MB,最大图像大小为 10MB。

  • 同时,假设文本上传与图像上传的比例为 9:1

文本上传

  • 文本上传数量 = 180K

  • 最大文本上传大小 – 5MB

  • 平均文本上传大小 – 10KB

  • 每日总大小 = 10KB * 180K = 每日 1.8 GB

  • 假设没有粘贴会过期,那么 3 年所需的总大小为 = 1.83365 ~= 2TB

图像上传

  • 图像上传数量 = 20K

  • 最大文本上传大小 – 10MB

  • 平均图像上传大小 – 100KB

  • 每日总大小 = 100KB * 20K = 每日 2 GB

  • 假设没有粘贴会过期,那么 3 年所需的总大小为 = 23365 ~= 2.2TB

所需总大小 = 2TB + 2.2TB  ~= 4.2 TB(三年内)

数据库架构

我们需要存储用户创建的每个粘贴本以及相应的文本或图像。如前所述,文本大小可以达到 5MB,图像大小可以达到 10MB。

我们不能将图像存储在数据库中,否则会是一个糟糕的设计,因为这样会涉及大量的数据库 IO。同时,如果文本的大小较大,存储文本也不应放入数据库。因此,以下策略可以用于文本和图像的存储。

文本存储

  • 如果文本小于 10 KB,则可以将其作为数据库的一部分存储。

  • 如果不是,则文本将存储在 Blob 存储中。我们可以在这里使用 Amazon S3。S3 的链接将存储在数据库中

图像存储

  • 图像将始终存储在 S3 中

我们没有任何 ACID 要求,因此可以使用 No SQL 数据库来存储将要创建的粘贴。在这种情况下,我们可以使用 Cassandra 数据库。

我们将有以下表

  • 用户表

  • 粘贴表

用户表

它将包含以下字段

  • user_id

  • user_name

  • password_encrypted

  • created

  • updated

粘贴表

它将包含以下字段

  • paste_id – 它将是一个 UUID

  • paste_type – 它可以是文本或图像

  • text – 如果 paste_type 是文本并且文本大小小于 10KB,则将填充该字段

  • s3_url – 在两种情况下填充。如果 paste_type 是文本且文本大小大于 10KB,或者 paste_type 是图像时。

  • user_id

  • created

  • updated

如何生成唯一的 URL

由于粘贴是为了创建并与其他用户共享,如果我们能够生成一个短链接以方便分享会更好。因此,我们现在将查看如何为粘贴生成短链接。为此,我们将有另一个服务,称为密钥生成服务,用于生成一个短密钥,该密钥将用于创建的粘贴 URL。

高层次设计

高层次地,我们先讨论一下更高层次的流程以及将会存在的所有服务。

  • 将会有一个 API 网关,所有用户的请求都会通过这个网关。

  • 将会有一个 Pastebin 服务。该服务负责生成所有粘贴 URL。

  • 将会有一个 Key Generation Service 服务,负责生成短密钥

  • Pastebin 服务将在需要新的密钥时调用密钥生成服务。

  • Pastebin 服务用尽所有密钥范围时,它将发布一个 Kafka/SNS 消息,说明密钥范围已被用尽。

  • 该消息将由 Key Recovery 服务接收,该服务是一个工作进程。它将标记密钥范围为可用,这样可以再次选择该范围。该工作进程还将从数据库中删除所有密钥范围内创建的粘贴。

  • 我们将缓存最新创建的粘贴,因为它们更有可能在创建后被共享和访问

以下是该服务的高层次图示

让我们看一下 Paste Bin 服务和 Key Generation 服务的一些细节

Paste Bin 服务

这个服务将成为我们系统中所有 API 的接口。PasteBin 服务将暴露一个创建粘贴的 API,并且还会暴露一个读取粘贴的 API。

创建粘贴 – 它将与密钥生成服务交互,获取短密钥。这些密钥将用于生成 URL。然后,它将为该粘贴在数据库中创建一个条目,并且还会在缓存中创建一个条目。

读取粘贴 – 它将首先检查粘贴是否已存在于缓存中。如果存在,它将返回该粘贴;如果不存在,它将从数据库中获取粘贴。

提高创建和读取粘贴的效率:当文本是 KB 级别或粘贴中包含图片时,通过 PasteBin 服务创建和读取粘贴会面临挑战。在这种情况下,我们可以进行一种优化:直接将大文本或图片上传到 Blob 存储(如 Amazon S3 或 HDFS)。我们是如何做到的呢?

上传

  • 假设用户 A 想要创建一个包含图片的粘贴。客户端将向服务器发送请求,请求一个预签名 URL,以便客户端上传图片。

  • 服务器将响应一个预签名 URL,其有效期可能是几小时。你可以阅读这篇文章了解预签名 URL 的相关信息:docs.aws.amazon.com/AmazonS3/latest/userguide/ShareObjectPreSignedURL.html。基本上,这个 URL 已经用令牌签名,因此可以直接用于上传,无需进一步的身份验证。这也叫做直接上传。服务器还将返回image_id

  • 客户端将把图片上传到该 URL。它将直接存储到 S3 中。

  • 现在客户端将调用创建粘贴 API。在调用创建粘贴 API 时,它也会在请求体中传递image_id。服务器将接收到该image_id。由于它有image_id,它将知道与粘贴对应的图片已经上传到 Blob 存储,这里指的是 S3。它会在将粘贴保存到数据库时,将路径作为字段保存。

通过上述方法,我们避免了通过 PasteBin 服务传输大量的图片或文本字节,从而在成本和性能上进行了一种优化。

下载

  • 用户 A 已创建一个粘贴,并将与用户 B 共享。所以用户 B 将阅读该粘贴。

  • 在读取时,过程正好相反。PasteBin 服务从缓存或数据库中获取粘贴内容。

  • 一旦它获取到粘贴内容,它将检查是否包含大文本或图片。如果是,它将从数据库中获取 S3 位置。

  • 然后,它会为该 S3 位置生成一个预签名 URL。客户端将同时返回粘贴信息和 S3 URL。

  • 使用这个预签名 S3 URL,客户端可以直接下载相应的大粘贴或图片文件。

密钥生成服务

将会有一个 KGS 服务,负责生成键。首先,让我们看看每个键的长度应该是多少。可能的长度选项为 6、7、8。只能使用 Base64 URL 安全字符来生成键。因此,

  • 对于 6—我们有 64⁶ = 68.7 亿个选项

  • 对于 7—我们有 64⁷ = 约 3500 亿个选项

  • 对于 8—我们有 64⁸ = 万亿个选项

我们现在可以假设 687 亿条记录足够,所以我们可以为键分配 6 个字符。现在的问题是这些键如何在数据库中进行维护。如果我们在数据库中存储 687 亿条记录,可能会有太多条记录,且浪费资源。

一种选择是将键范围存储在数据库中。我们可以有一个 64 的范围,在该范围内仅存储前五个字符,这些字符将作为前缀,用于从该前缀生成的所有 64 个键。

假设我们有以下前缀

adcA2

然后可以从此生成下列 64 个键

  • adcA2[a-z] – 26 个键

  • adcA2[A-Z] – 26 个键

  • adcA2[0-9] – 10 个键

  • adcA2[-_] – 2 个键

我们可以将这些范围存储在数据库中。因此,对于 6 个字符,我们将总共在数据库中存储 64⁵ 条记录。键将通过键服务按范围和批次返回给 Tiny URL 服务。Tiny URL 服务然后使用此前缀生成 64 个键,并处理 64 个不同的创建短网址请求。这是优化,因为PasteBin服务只有在用尽所有 64 个键时才需要调用键生成服务。所以PasteBin服务将向键生成服务发出一次调用来生成 64 个短网址。接下来我们来看 KGS 服务的要点

  • 数据库架构

  • 使用哪个数据库

  • 如何解决并发问题

  • 如何恢复 key_prefix

  • 如果键范围耗尽会发生什么情况

  • 如果粘贴内容永不过期怎么办

  • KGS 服务不是单点故障吗?

数据库架构

将只有一个表来存储键范围,即前缀。以下是该表中的字段

  • key_prefix

  • key_length – 目前始终为 6。这个字段存在,是为了在任何情况下需要 7 个长度的键时使用。

  • used – 如果为真,则表示键前缀当前正在使用中。如果为假,则表示可以使用。

  • created

  • updated

使用哪个数据库

我们没有任何 ACID 要求,因此可以使用 No SQL 数据库。此外,我们可能还需要保存非常大的数据,所以 No SQL 可能更合适。这个系统将是一个写重型和读重型的系统。所以我们可以在这里使用Cassandra 数据库。我们可以根据数据库的容量估算来决定我们想要的分片数。每个分片也会被适当复制。

这里还有一个优化可以减少延迟。我们可以在缓存中重新填充空闲的键范围,KGS 服务可以直接从缓存中获取,而不是每次都去数据库。

如何解决并发问题

很有可能发生两个请求看到相同的前缀或范围为空闲状态。由于多个服务器同时从密钥数据库读取,我们可能会遇到两个或多个服务器同时将相同的密钥视为空闲的情况。我们刚才提到的并发问题有两种方式可以解决。

  • 两台或更多的服务器读取相同的密钥,但只有一台服务器能在数据库中将该key_prefix标记为已使用。并发性发生在数据库级别,即每一行在更新之前都会被锁定,我们可以在这里利用这一点。数据库将返回给服务器,告知记录是否已被更新。如果记录未被更新,则服务器可以获取新的密钥。如果记录已更新,则该服务器已获得正确的密钥。

  • 另一种选择是使用一个事务,在一个事务中完成查找和更新。每次查找和更新都会返回一个唯一的 key_prefix。这可能不是推荐的选项,因为它会增加数据库的负担

如何恢复 key_prefix

一旦 Tiny URL 服务耗尽了密钥范围,它将把该范围插入到另一个表中,经过 2 周后可以从中恢复并重新标记为空闲。我们可以确定,经过两周后这些密钥会是空闲的,因为我们设置了 2 周的过期时间。

如果密钥范围耗尽会发生什么

这将是一个意外情况。将有一个后台工作进程检查密钥范围是否耗尽。如果是,它可以为 7 位长度的密钥生成新范围。但如何知道密钥范围是否耗尽呢?为了保持粗略计数,可能会有另一个表用来存储已使用密钥的用户数量。

  • 每当 KGS 将一个范围分配给 Tiny URL 服务时,它会发布一条消息,该消息会被一个同步工作进程接收,该工作进程将减少已使用密钥的计数。

  • 同样,每当一个范围是空闲的,我们可以增加这个计数器。

如果 PasteBin 永不过期会怎样

很容易扩展上述服务来处理永不过期的粘贴内容。

  • 只是我们的短字符串不会局限于 6 位字符。我们可以根据需要使用 7 位、8 位甚至 9 位字符。

  • 将不提供密钥恢复服务

  • 一旦key_range被分配,我们可以将其从密钥数据库中移除,因为它永远不需要被释放或恢复。

KGS 服务不是单点故障吗?

为了防止这种情况发生,我们将对密钥数据库进行适当的复制。此外,服务本身将有多个应用服务器。我们还将设置适当的自动扩展机制,并可以进行灾难恢复管理。

其他常见组件

其他常见组件可能包括

  • 用户服务 – 它保存用户的个人信息。

  • 令牌/认证服务 – 用户令牌的管理

  • 短信服务 – 用于将任何类型的消息发送回用户。例如 – OTP

  • 分析服务 – 这可以用来追踪任何类型的分析

非功能性需求

现在让我们讨论一些非功能性需求。

可扩展性

在上述设计中需要首先考虑的因素是可扩展性。系统中每个组件的可扩展性非常重要。以下是您可能会遇到的可扩展性挑战及其可能的解决方案:

  • paste_bin服务和KGS服务中的每台机器只能处理有限数量的请求。因此,每个服务都应该有合适的自动扩展设置,以便根据请求的数量,我们可以添加实例并在需要时进行自动扩展。

  • 您的 Kafka/SNS 系统可能无法承受如此大的负载。我们可以进行水平扩展,但有其限度。如果这成为瓶颈,那么根据地理位置或用户 ID,我们可以拥有两个或更多这样的系统。可以使用服务发现来确定请求应该发送到哪个 Kafka 系统。

  • 可扩展性的另一个重要因素是,我们设计系统时确保没有任何服务被过多的任务拖累。我们进行了关注点分离,并且在任何服务承担过多责任的地方,都进行了拆分。

低延迟

  • 我们可以缓存新创建的 paste,并设置一定的过期时间。每当一个 paste 被创建时,它更可能在短时间内被访问。这样可以减少许多读取请求的延迟。

  • 我们还创建了键或键范围的批次。这可以防止 Paste Bin 服务每次都调用 KGS 服务,整体上提高了延迟表现。

  • 还有一个优化可以在这里进行,以提高延迟。我们可以在缓存中重新填充空闲的键范围,KGS 服务可以直接从缓存中获取数据,而不是每次都去访问数据库。

可用性

为了使系统具有高可用性,几乎所有组件都需要冗余/备份。以下是需要完成的一些事项。

  • 对于我们的数据库,需要启用复制。每个主分片节点应该有多个从节点。

  • 对于 Redis,我们也需要实现复制。

  • 对于数据冗余,我们也可以实现多区域部署。如果某个区域出现故障,这将是其中一个好处。

  • 还可以设置灾难恢复

警报和监控

警报和监控也是非常重要的非功能性需求。我们应该监控每一个服务,并设置合适的警报。可以监控的内容包括:

  • API 响应时间

  • 内存消耗

  • CPU 消耗

  • 磁盘空间消耗

  • 队列长度

  • ….

向用户位置靠近

这里可以采用几种架构方案。其中之一是单元架构。您可以在这里阅读更多关于单元架构的内容:github.com/wso2/reference-architecture/blob/master/reference-architecture-cell-based.md

避免单点故障

单点故障是指系统中某个部分停止工作时,会导致整个系统崩溃的情况。我们在设计时应该尽量避免任何单点故障。通过冗余设计和多区域部署,我们可以防止这种情况发生。

结论

这篇文章是关于Pastebin服务的系统设计。希望你喜欢这篇文章。请在评论中分享反馈。

数据库中的强一致性意味着什么

原文:techbyexample.com/strong-consistency-databases/

目录

  • 概述

  • 强一致性的公式

    • 节点或副本的数量

    • 写法定人数

    • 读取法定人数

  • 节点数量为 1

  • 节点数量为 2

    • 写法定人数为 2,读法定人数为 1

    • 写法定人数为 1,读法定人数为 2

    • 写法定人数为 2,读法定人数为 2

    • 写法定人数为 1,读法定人数为 1

  • 节点数量为 3

    • 写法定人数为 2,读法定人数为 2

    • 写法定人数为 2,读法定人数为 1

    • 写法定人数为 1,读法定人数为 2

  • 强一致性的缺点

    • 一致性是如何实现的

    • 可用性是如何实现的

  • 何时需要强一致性?

概述

简单来说,如果一个系统在读取时总是获取最新的写入数据,那么它就是强一致的。它永远不会返回过时或陈旧的值。根据维基百科,强一致性的定义是:

如果协议支持强一致性,则意味着:所有访问都按相同顺序(顺序)由所有并行进程(或节点、处理器等)看到。因此,只有一个一致的状态可以被观察到,而与弱一致性不同,弱一致性下不同的并行进程(或节点等)可能会在不同的状态下感知变量。

强一致性的公式

要理解数据库中的强一致性,我们首先需要理解三个术语:

  • 节点或副本的数量

  • 写法定人数

  • 阅读法定人数

节点或副本的数量

这是你系统中存在的节点数量。当我们说节点时,实际上是指具有相同数据的副本数量

写法定人数

它是发生写入并被视为成功返回的最小节点数。

读取法定值

它是发生读取并被视为成功返回的最小节点数。如果这些最小节点返回的值不同,则该读取将被拒绝

从数学上讲,如果写入法定值读取法定值的和大于读取节点的数量,那么系统被认为是强一致的。

也就是说,如果写入法定值为 W,读取法定值为 R,节点数量为 N,则

  • 如果 W+R > N,则系统是强一致的

  • 如果 W+R<=N,则系统不能保证强一致性

换句话说,如果写入法定值节点和读取法定值节点之间有一个公共节点,那么系统将是强一致的。这个公共节点将拒绝过时的读取。每当 W+R > N 时,写入法定值和读取法定值之间总会有一个公共节点。要理解这一点在数学上并不困难。

让我们通过一些示例来看一下这个公式是如何成立的。我们将看到以下几种情况。

  • 节点数量为 1

  • 节点数量为 2

  • 节点数量为 3

节点数量为 1

在这种情况下,唯一可能的写入和读取法定值组合是 1 和 1。写入法定值和读取法定值的和大于读取节点的数量。由于只有一个实例,因此读取总是最新的,因为读取和写入发生在同一个实例上

节点数量为 2

在此,还有四种情况。

  • 写入法定值为 2,读取法定值为 1 – 强一致

  • 写入法定值为 1,读取法定值为 2 – 强一致

  • 写入法定值为 2,读取法定值为 2 – 强一致

  • 写入法定值为 1,读取法定值为 1 – 非强一致

写入法定值为 2,读取法定值为 1

在这种情况下,写入法定值和读取法定值的和为 3,大于节点的数量,因此系统应当是强一致的。在这种情况下,由于写入发生在两个节点上,因此从任意一个节点读取都会返回相同的数据,系统整体上将是强一致的

写入法定值为 1,读取法定值为 2

再次,在这种情况下,写入仲裁数和读取仲裁数的总和为 3,超过了节点数量,因此系统应保持强一致性。在这种情况下,写入仅发生在一个实例上。但读取发生在两个节点上。假设有一个名为 A 的数据,其初始值为 1。两个节点的 A 值都是 1。现在在第一个节点上进行了写入,A 的值被更改为 2。由于写入仲裁数为 1,写入将在第一个节点上进行并成功返回。假设在节点 2 与节点 1 的最新数据同步之前,发生了读取操作。由于读取仲裁数为 2,它将从两个节点读取。第一个节点将返回 A 的值为 2,而第二个节点将返回 A 的值为 1。由于两个节点返回的值不同,系统将拒绝该读取操作以保持强一致性。

写入仲裁数为 2,读取仲裁数为 2

再次,在这种情况下,写入仲裁数和读取仲裁数的总和为 4,超过了节点数量,因此系统应保持强一致性。在这种情况下,由于写入发生在两个节点上且读取也来自两个节点,因此每次读取将是最新的。

写入仲裁数为 1,读取仲裁数为 1

再次,在这种情况下,写入仲裁数和读取仲裁数的总和为 2,等于节点数量,因此系统不是强一致的。写入发生在节点 1 上,在数据同步到节点 2 之前,节点 2 上发生了读取操作。这是过时的数据。

节点数量为 3

在这种情况下,可以有多种情况,但我们只讨论其中几种。

  • 写入仲裁数为 2,读取仲裁数为 2 – 强一致性

  • 写入仲裁数为 2,读取仲裁数为 1 – 非强一致性

  • 写入仲裁数为 1,读取仲裁数为 2 – 非强一致性

写入仲裁数为 2,读取仲裁数为 2

在这种情况下,写入仲裁数和读取仲裁数的总和为 4,超过了节点数量,因此系统应保持强一致性。假设有三个节点:节点 1节点 2节点 3

再假设一个情况,其中名为 A 的数据的初始值为 1。对节点 1 和节点 2 进行了写入操作,A 的值增加至 2。在数据同步到节点 3 之前,发生了读取操作。对于读取,可能会有三种情况。

  • 读取来自节点 1 和节点 2

  • 读取来自节点 1 和节点 3

  • 读取来自节点 2 和节点 3

a. 读取来自节点 1 和节点 2

在这种情况下,没有问题,A 的值将在两个节点中均返回为 2,系统将保持强一致性。

b. 读取来自节点 1 和节点 3

在这种情况下,节点 1 将返回值 2,而节点 3 将返回值 A 为 1。由于这两个值不同,读取将被拒绝,系统将保持强一致性。

c. 读取来自节点 2 和节点 3

在这种情况下,节点 2 将返回值为 2,而节点 3 将返回值为 1。由于这两个值不同,读取将被拒绝,系统将保持强一致性。

写入仲裁数为 2,读取仲裁数为 1

在这种情况下,写入仲裁数和读取仲裁数的总和为 3,而该总和不大于系统中的节点数,因此系统不应该保持强一致性。

写入发生在节点 1 和节点 2,A 的值更新为 2。读取发生在节点 3,返回的值为 1,使得系统既不保持强一致性也不保持弱一致性。

写入仲裁数为 1,读取仲裁数为 2

写入发生在节点 1,A 的值更新为 2。读取发生在节点 2 和节点 3,两个节点都返回 A 的值为 1。由于返回值相同,读取没有被拒绝,系统不保持强一致性。

强一致性的缺点

只有在网络分区的情况下,才能以牺牲可用性为代价实现强一致性。这也是 CAP 定理的内容。CAP 定理指出,在分布式系统中,你只能实现以下三种属性中的两种:

  • 一致性

  • 可用性

  • 分区容错

实际上,很难实现分区容错,因为网络注定会失败,节点之间的通信注定会中断。这也是为什么在一个不具备分区容错的分布式系统中,只能实现一致性或可用性中的一种。

让我们举个例子来理解为什么必须牺牲其中之一。假设系统中有两个节点,它们相互连接,并且都处于同步状态。

现在假设两个节点之间发生了网络分区。

如何实现一致性

这里节点数为 2。因此,为了实现强一致性,我们有三种选择。

  • 写入仲裁数为 2,读取仲裁数为 1。

  • 写入仲裁数为 1,读取仲裁数为 2。

  • 写入仲裁数为 2,读取仲裁数为 2。

在第一种情况下,由于写入仲裁数是 2,因此必须同时写入两个节点。但由于第二个节点不可用,因此它将拒绝写入操作。因此,系统无法进行写入。

在第二种情况下,由于读取仲裁数是 2,因此必须从两个节点读取。但由于第二个节点不可用,因此它将拒绝读取操作。因此,系统无法进行读取。

在第三种情况下,由于写入仲裁数是 2,因此必须同时写入两个节点,而由于读取仲裁数是 2,因此必须同时从两个节点读取。但由于第二个节点不可用,因此它将拒绝读写请求。因此,系统无法进行读写操作。

从这三个案例中,你可以推断出,在网络分区的情况下,我们以牺牲可用性为代价来实现强一致性,而网络分区是不可避免的。

如何实现可用性

为了实现可用性,我们必须放弃一致性。这里节点数为 2。因此,有一个选项使得系统不会强一致。

  • 写入法定人数是 1,读取法定人数是 1

在第一个例子中,由于写入法定人数(Write Quorum)为 1,因此它可以写入节点 1。并且读取法定人数(Read Quorum)也是 1,因此它可以从节点 1 读取。因此,系统是可用的。写入法定人数和读取法定人数之和为 2,这等于节点数。根据公式,当写入法定人数和读取法定人数之和小于或等于节点数时,系统就不是强一致的。因此,在这种情况下,我们有可用性,但系统不一致。

何时强一致性是可取的?

有一些使用场景需要强一致性。例如,在银行交易的情况下,每次读取时都希望得到最新的值。

这就是关于数据库中的强一致性。希望你喜欢这篇文章。

注意: 另外,查看我们的系统设计教程系列 – 系统设计教程系列

分布式系统中的 CAP 定理

原文:techbyexample.com/cap-theorem-distributed-systems/

目录

  • 概述

    • 一致性

    • 可用性

    • 分区容忍

  • CAP 定理

    • 一致性与可用性

    • 一致性与分区容忍

    • 可用性与分区容忍

  • 强一致性的公式

    • 节点或副本的数量

    • 写入法定人数

    • 读取法定人数

  • 节点数量为 1

  • 节点数量为 2

    • 写入法定人数为 2,读取法定人数为 1

    • 写入法定人数为 1,读取法定人数为 2

    • 写入法定人数为 2,读取法定人数为 2

    • 写入法定人数为 1,读取法定人数为 1

  • CAP 定理的证明

    • 如何在牺牲可用性的情况下实现一致性

    • 如何在牺牲一致性的情况下实现可用性

  • 结论

概述

分布式系统是由多个节点通过网络相互连接的系统。CAP 定理是分布式系统中的一个基本定理,包含三项内容

  • C 代表一致性

  • A 代表可用性

  • P 代表分区容忍性

该定理指出,你只能实现提到的三个目标中的两个。为什么会这样呢?在理解为什么只能实现其中两个之前,让我们先理解一下上述每个术语的含义。

一致性

这意味着所有节点的数据是一致的。本质上,所有节点看到的数据视图是相同的。所以每次读取都将返回最新的写入数据。一旦某个值的写入成功,随后的读取将返回该值。

可用性

你的分布式系统应该能够在合理的时间内对每一个请求发送一个无错误的响应。这个响应不一定是最新的。

分区容忍性

当发生网络分区时,系统仍然可以继续工作。这意味着,即使节点之间存在网络问题,出现消息失败、数据包丢失,系统仍然会继续运行。并不是系统会关闭。换句话说,系统是容忍网络分区的。

理想情况下,系统应该容忍网络分区。

现在我们已经理解了所有术语,让我们看看 CAP 定理是如何描述的。

CAP 定理

CAP 定理指出,在一个分布式系统中,你只能实现上述三个属性中的两个。

  • CA

  • CP

  • AP

换句话说,分布式系统不可能在同一时间内既是一致的、可用的,又是容忍分区的。

下面的图表中进行了说明

一致性和可用性

如果你的系统不容忍网络分区,那么它有可能同时实现一致性和可用性。

一致性和分区容忍性

如果你的系统容忍网络分区,那么你可以以牺牲可用性的代价来实现一致性。

可用性和分区容忍性

如果你的系统容忍网络分区,那么你可以以牺牲一致性的代价来实现可用性。

正如我们之前提到的,系统必须是分区容忍的,因为网络必然会失败,节点之间的通信必定会中断。系统应该在网络失败或消息丢失的情况下继续运行。因此,P对于系统来说应该始终为真,因此我们只能实现CA中的一个。换句话说,当你的系统是分区容忍时,只有一致性可用性可以实现。

让我们通过一个示例来理解为什么当系统是分区容忍时,必须牺牲一致性可用性中的一个。在本教程中,我们将通过一个示例来证明 CAP 定理。

但是即便要正确理解示例,我们首先需要准确理解CAP中的一致性一词。当我们在这里提到一致性时,我们指的是强一致性。

如果一个系统强一致,它就会读取到最新的写入数据。它永远不会返回过时或陈旧的值。举个例子,假设有一个分布式系统,包含两个节点。我们将看到一致性情况和不一致性情况的示例。

一致性案例示例

节点 n0 和 n1 都包含数据项'A'的值为 1。从任一节点读取都将返回'A'的值为 1。现在,'A'的值在两个节点上都被更新为 2。再次从任一节点读取,将返回'A'的值为 2。一切都是一致的。

不一致性案例示例

节点 n0 和 n1 都包含数据项 ‘A’ 的值为 1. 从任一节点读取都会返回值为 1. 现在,‘A’ 的值在第一个节点上更新为 2,但在第二个节点上没有更新。因此,从第一个节点读取将返回值为 2,而从第二个节点读取将返回值为 1。这是一个不一致状态的例子

现在让我们来看强一致性的公式

强一致性的公式

要理解数据库中的强一致性,我们首先需要理解三个术语

  • 节点或副本的数量

  • 写入法定人数

  • 读取法定人数

节点或副本的数量

这是你系统中存在的节点数量。这里所说的节点,实质上指的是具有相同数据的副本数量

写入法定人数

这是写入成功返回之前必须进行写入的最小节点数量。

读取法定人数

这是读取发生之前必须进行写入的最小节点数量。如果这些最小节点返回的值不同,则该读取将被拒绝

从数学角度看,如果 写入法定人数读取法定人数 之和大于读取节点的数量,那么系统被认为是强一致的。

也就是说,如果写入法定人数为 W,读取法定人数为 R,节点数量为 N,那么

  • 如果 W+R > N,则系统是强一致的

  • 如果 W+R<=N,则系统无法保证强一致性

换句话说,如果写入法定人数节点和读取法定人数节点之间有一个共同节点,那么系统将是强一致的。这个共同节点将拒绝过时的读取。当 W+R > N 时,写入法定人数和读取法定人数之间总会有一个共同节点。从数学上来说,为什么是这样并不难理解。

让我们通过一些例子来看这个公式是如何成立的。我们将看到以下几种情况。

  • 节点数量为 1

  • 节点数量为 2

  • 节点数量为 3

节点数量为 1

在这种情况下,唯一可能的写入和读取法定人数组合是 1 和 1. 写入法定人数和读取法定人数之和大于读取节点的数量。由于只有一个实例,读取总是最新的,因为读取和写入都发生在同一个实例上

节点数量为 2

在此,有进一步的四种情况。

  • 写入法定人数为 2,读取法定人数为 1 – 强一致性

  • 写入法定人数为 1,读取法定人数为 2 – 强一致性

  • 写入法定人数为 2,读取法定人数为 2 – 强一致性

  • 写入法定人数为 1,读取法定人数为 1 – 不强一致

写入法定人数为 2,读取法定人数为 1

在这种情况下,写入法定人数和读取法定人数之和为 3,这大于节点数量,因此系统应该是强一致的。在这种情况下,由于写入发生在两个节点上,因此从任一节点读取都将返回相同的数据,系统整体将是强一致的

写入法定人数为 1,读取法定人数为 2

再次强调,在这种情况下,写操作法定数和读操作法定数的和为 3,这大于节点的数量,因此系统应该是强一致性的。在这种情况下,写操作只在一个节点上进行。但是,读操作来自两个节点。假设有一个名为 A 的数据,它的初始值是 1。两个节点的 A 的值都是 1。现在,在第一个节点上发生了写操作,将 A 的值更改为 2。由于写操作法定数是 1,因此写操作将在第一个节点上执行并成功返回。假设在节点 2 同步到节点 1 上的最新数据之前,发生了一次读操作。由于读操作法定数是 2,因此它将从两个节点读取数据。第一个节点将返回 A 的值为 2,而第二个节点将返回 A 的值为 1。由于两个节点返回的值不相同,系统将拒绝此次读操作,以保持强一致性。

写操作法定数是 2,读操作法定数是 2

再次强调,在这种情况下,写操作法定数和读操作法定数的和为 4,这大于节点的数量,因此系统应该是强一致性的。在这种情况下,由于写操作发生在两个节点上,且读操作也来自两个节点。因此,每次读取都会是最新的。

写操作法定数是 1,读操作法定数是 1

再次强调,在这种情况下,写操作法定数和读操作法定数的和为 2,等于节点的数量,因此系统不是强一致性的。写操作发生在节点 1 上。数据还没有同步到节点 2 时,节点 2 上发生了读操作。这个读操作的数据是过时的。

CAP 定理的证明

现在你已经了解了强一致性,让我们更好地理解一下 CAP 定理。假设系统中有两个节点,它们相互连接并且保持同步。

正如我们从上面的公式中已经知道的,当系统有两个节点时,它将会是强一致性的,当

  • 写操作法定数是 2,读操作法定数是 1

  • 写操作法定数是 1,读操作法定数是 2

  • 写操作法定数是 2,读操作法定数是 2

系统将不会是强一致性的,当

  • 写操作法定数是 1,读操作法定数是 1

我们之前提到过,在一个具有分区容忍性的分布式系统中,只有在一致性或可用性之间做出选择。让我们来看一下是如何实现的。假设两个节点之间发生了网络分区,并且第二个节点不可用。

如何以牺牲可用性来实现一致性

这里节点的数量是 2。因此,为了实现强一致性,我们有三种选择

  • 写操作法定数是 2,读操作法定数是 1

  • 写操作法定数是 1,读操作法定数是 2

  • 写操作法定数是 2,读操作法定数是 2

在第一种情况中,由于写操作法定数是 2,因此必须写入两个节点。但由于第二个节点不可用,写操作将被拒绝。因此,系统在写操作上不可用。

在第二种情况下,由于读操作法定人数为 2,因此必须从两个节点读取数据。但是,由于第二个节点不可用,因此会拒绝读取操作。因此,系统在读取操作上不可用。

在第三种情况下,由于写操作法定人数为 2,因此必须向两个节点写入数据;同时,由于读操作法定人数为 2,因此必须从两个节点读取数据。但是,由于第二个节点不可用,因此会拒绝读写操作。因此,系统在读写操作上都不可用。

从这三种情况中,可以推断出,在网络分区的情况下,我们以牺牲可用性为代价获得强一致性。因此,在这种情况下,我们只能实现CP。在任何情况下我们都无法获得A

如何以牺牲一致性为代价实现可用性

为了实现可用性,我们必须放弃一致性。这里的节点数量为 2。因此,有一种情况系统将不具备强一致性。

  • 写操作法定人数(Write Quorum)为 1,读操作法定人数(Read Quorum)为 1

在第一种情况下,由于写操作法定人数为 1,因此可以向节点 1 写入数据。同时,由于读操作法定人数为 1,因此可以从节点 1 读取数据。因此,系统是可用的。写操作法定人数和读操作法定人数的总和为 2,等于节点的数量。根据公式,当写操作法定人数和读操作法定人数的总和小于等于节点的数量时,系统就不具备强一致性。因此,在这种情况下,系统是可用的,但不一致。

因此,在这种情况下我们只能实现AP。我们无法获得C

这是 CAP 定理的证明

结论

这就是分布式系统中的 CAP 定理。希望你喜欢这篇文章。请在评论中分享反馈。

注意: 查看我们的系统设计教程系列 系统设计问题

在一组字符串中查找最长公共前缀

原文:techbyexample.com/longest-common-prefix-program/

概述

给定一个字符串数组,目标是从该数组中找到最长公共前缀。如果没有公共前缀,应该输出一个空字符串。

示例 1

Input: ["fan", "fat", "fame"]
Output: "fa"

示例 2

Input: ["bat", "van", "cat"]
Output: ""

程序

以下是相同的程序

package main

import "fmt"

func longestCommonPrefix(strs []string) string {
	lenStrs := len(strs)

	if lenStrs == 0 {
		return ""
	}

	firstString := strs[0]

	lenFirstString := len(firstString)

	commonPrefix := ""
	for i := 0; i < lenFirstString; i++ {
		firstStringChar := string(firstString[i])
		match := true
		for j := 1; j < lenStrs; j++ {
			if (len(strs[j]) - 1) < i {
				match = false
				break
			}

			if string(strs[j][i]) != firstStringChar {
				match = false
				break
			}

		}

		if match {
			commonPrefix += firstStringChar
		} else {
			break
		}
	}

	return commonPrefix
}

func main() {
	output := longestCommonPrefix([]string{"fan", "fat", "fame"})
	fmt.Println(output)

	output = longestCommonPrefix([]string{"bat", "van", "cat"})
	fmt.Println(output)
}

输出:

"fa"
""

注意: 另外,查看我们的系统设计教程系列:系统设计教程系列

chmod o-w 命令在 bash 或终端中是什么意思?

原文:techbyexample.com/chmod-ow/

概述

在管理文件权限时,图示中有三个组成部分。

权限组

  • 用户 – 缩写为‘u’

  • – 缩写为‘g’

  • 其他– 缩写为‘o’

  • 所有 – 缩写为‘a’

权限类型

  • 读取权限 – 缩写为‘r’

  • 写权限 – 缩写为‘w’

  • 执行权限 – 缩写为‘x’

操作定义

*** + 用于添加权限

  • 用于移除权限。

  • = 用于设置权限。

所以o-w表示从其他用户那里撤销权限。

在查看示例之前,让我们先看看当你运行ls命令时文件权限是如何表示的。

以下是关于上述图示的一些要点:

  • 第一个字符代表文件类型。‘-’表示常规文件,‘d’表示目录。

  • 第二到第四个字符代表文件所有者的读、写和执行权限。

  • 第四到第七个字符代表组的读、写和执行权限。

  • 第八到第十个字符代表其他用户的读、写和执行权限。

示例

  • 创建一个文件temp.txt,检查其权限。
ls -all | grep temp.txt
-rw-r--r--    1 root  root      0 Aug  9 14:50 temp.txt

注意其他用户仅有读取权限。

  • 现在运行命令为其他用户授予写权限。

chmod o+w temp.txt

ls -all | grep temp.txt

-rw-r–rw- 1 root root 0 8 月 9 日 14:50 temp.txt

请参见上面的输出。也为其他用户授予了执行权限。

  • 现在运行命令撤销其他用户的写权限。

chmod o-w temp.txt

ls -all | grep temp.txt

-rw-r–r– 1 root root 0 8 月 9 日 14:50 temp.txt

注意如何从其他用户那里撤销写权限。

注意: 查看我们的系统设计教程系列 系统设计问题

最大长度的连续子数组,包含相等数量的 0 和 1。

原文:techbyexample.com/max-length-array-zero-one/

概览

给定一个数组,其中仅包含 0 和 1。目标是找到一个最大长度的子数组,包含相等数量的 0 和 1。让我们通过示例来理解。

示例 1

Input: intervals = [0, 1]
Output: 2

示例 2

Input: intervals = [0, 1, 1, 0, 1, 1]
Output: 4

下面是我们可以采用的方法

  • 我们将创建一个数组 leftSum,其中 leftSum[i] 表示从索引 0 到 i 的数字之和。将 0 视为 -1,1 视为 1。

  • 现在有两种情况。子数组要么从索引 0 开始,要么从其他非零的索引开始。

  • 从左到右扫描 leftSum 数组。如果 leftSum 数组中某个索引的值为零,那么意味着子数组[0,i]中包含相等数量的 0 和 1。这将给出子数组从索引 0 开始的答案。

  • 如果子数组不是从零开始的。再次扫描 leftSum 数组,找到索引 ij,使得 leftSum[i] == leftSum[j]。为了找出这个情况,我们将使用一个映射。如果 j-i 的长度大于当前最大长度,则更新最大长度。

  • 最后返回最大长度

程序

这里是相应的程序。

package main

import "fmt"

func findMaxLength(nums []int) int {
	lenNums := len(nums)

	if lenNums == 0 {
		return 0
	}

	currentSum := 0

	sumLeft := make([]int, lenNums)

	for i := 0; i < lenNums; i++ {
		if nums[i] == 0 {
			currentSum = currentSum - 1
		} else {
			currentSum = currentSum + 1
		}
		sumLeft[i] = currentSum
	}

	maxLength := 0

	max := 0
	min := 0

	for i := 0; i < lenNums; i++ {
		if sumLeft[i] == 0 {
			maxLength = i + 1
		}
		if sumLeft[i] > max {
			max = sumLeft[i]
		}

		if sumLeft[i] < min {
			min = sumLeft[i]
		}
	}

	numMap := make(map[int]int, max-min+1)

	for i := 0; i < lenNums; i++ {
		index, ok := numMap[sumLeft[i]]

		if ok {
			currentLength := i - index
			if currentLength > maxLength {
				maxLength = currentLength
			}
		} else {
			numMap[sumLeft[i]] = i
		}
	}

	return maxLength

}
func main() {
	output := findMaxLength([]int{0, 1})
	fmt.Println(output)

	output = findMaxLength([]int{0, 1, 1, 0, 1, 1})
	fmt.Println(output)
}

输出

2
4

两栋最远的颜色不同的房子

原文:techbyexample.com/two-furthest-houses-different-colors/

概述

给定一个数组,表示房子的颜色。数组[i]表示索引 i 处的房子的颜色。目标是找到两栋最远的颜色不同的房子。

如果没有这样的索引存在,则返回 -1

示例 1

Input: intervals = [2,2,2,1,2,2]
Output: 3
Explanation: House at index 0 and house at index 3 is of different colors

示例 2

Input: intervals = [1, 2 ,3, 1, 2]
Output: 2
Explanation: House at index 0 and house at index 4 is of different colors

下面是我们可以采取的方法。

  • 其中一栋房子将位于 0 索引或 n-1 索引处,其中 n 是数组的长度

  • 我们可以首先假设 0 索引处的房子在解中,然后进行计算

  • 我们可以接下来假设位于 n-1 索引的房子在解中,然后进行计算

程序

以下是相应的程序。

package main

import "fmt"

func maxDistance(colors []int) int {
	lenColors := len(colors)

	if lenColors == 0 || lenColors == 1 {
		return 0
	}
	maxDiff := 0

	leftColor := colors[0]
	rightColor := colors[lenColors-1]

	for i := 1; i < lenColors; i++ {
		if colors[i] != leftColor {
			maxDiff = i
		}
	}

	for i := lenColors - 2; i >= 0; i-- {
		if colors[i] != rightColor {
			diff := lenColors - i - 1
			if diff > maxDiff {
				maxDiff = diff
			}
		}
	}

	return maxDiff
}
func main() {
	output := maxDistance([]int{2, 2, 2, 1, 2, 2})
	fmt.Println(output)

	output = maxDistance([]int{1, 2, 3, 1, 2})
	fmt.Println(output)
}

输出

3
4

数组中递增元素之间的最大差值

原文:techbyexample.com/maximum-difference-increasing-elements/

概述

给定一个数组。目标是找出两个索引 i 和 j 之间的最大差值,使得

  • j > i

  • arr[j] > arr[i]

如果不存在这样的索引,则返回 -1

示例 1

Input: intervals = [8, 2, 6, 5]
Output: 4
Explanation: 6-2 = 4

示例 2

Input: intervals = [8, 3, 2, 1]
Output: -1
Explanation: Condition is not satified

程序

下面是相应的程序。

package main

import "fmt"

func maximumDifference(nums []int) int {
	lenNums := len(nums)
	if lenNums == 0 || lenNums == 1 {
		return -1
	}
	minElement := nums[0]

	maxDifference := -1

	for i := 1; i < lenNums; i++ {
		diff := nums[i] - minElement
		if diff > maxDifference && diff != 0 {
			maxDifference = diff
		}

		if nums[i] < minElement {
			minElement = nums[i]
		}
	}

	return maxDifference
}

func main() {
	output := maximumDifference([]int{8, 2, 6, 5})
	fmt.Println(output)

	output = maximumDifference([]int{8, 3, 2, 1})
	fmt.Println(output)

}

输出

4
-1

非重叠区间程序

原文:techbyexample.com/non-overlapping-intervals-program/

概述

给定一个区间数组,其中 intervals[i] = [starti, endi]。我们需要找出最少需要移除的区间数量,以使得区间数组中的区间变得不重叠。

让我们通过一个例子来理解

Input: intervals = [[2,3],[3,4],[4,5],[2,4]]
Output: 1
Explanation: [2,4] can be removed and the rest of the intervals are non-overlapping.

这个思路是先根据区间的开始时间排序,然后计算重叠的区间。

程序

以下是相应的程序。

package main

import (
	"fmt"
	"sort"
)

func eraseOverlapIntervals(intervals [][]int) int {
	lenIntervals := len(intervals)

	sort.Slice(intervals, func(i, j int) bool {
		return intervals[i][0] < intervals[j][0]
	})

	prevIntervalEnd := intervals[0][1]

	minIntervals := 0
	for i := 1; i < lenIntervals; i++ {
		currentIntervalStart := intervals[i][0]
		currentIntervalEnd := intervals[i][1]

		if currentIntervalStart < prevIntervalEnd {
			minIntervals++
			if prevIntervalEnd >= currentIntervalEnd {
				prevIntervalEnd = currentIntervalEnd
			}
		} else {
			prevIntervalEnd = currentIntervalEnd
		}
	}
	return minIntervals
}

func main() {

	output := eraseOverlapIntervals([][]int{{2, 3}, {3, 4}, {4, 5}, {2, 4}})
	fmt.Println(output)
}

输出

6
13

数组中的第 k 个不同字符串程序

原文:techbyexample.com/kth-distinct-string/

概述

给定一个输入字符串数组,可能包含重复字符串。还给定了一个输入数字k。目标是找到给定输入字符串数组中的第 k 个不同字符串

让我们通过一个例子来理解它

Input: ["a", "c", "b" , "c", "a", "b", "e", "d"]
k=2

Output: "d"

在上面的数组中,以下字符串是重复的

  • “a”

  • “c”

  • “b”

下面的字符串没有重复

  • “e”

  • “d”

由于字符串“d”是按顺序第二次出现,并且 k 为 2,因此输出是“d”另一个例子

Input: ["xxx", "xx" "x"]
k=2

Output: "xx"

由于与上面相同的推理

程序

这是相同功能的程序。

package main

import "fmt"

func rob(nums []int) int {
	lenNums := len(nums)

	if lenNums == 0 {
		return 0
	}

	maxMoney := make([]int, lenNums)

	maxMoney[0] = nums[0]

	if lenNums > 1 {
		maxMoney[1] = nums[1]
	}

	if lenNums > 2 {
		maxMoney[2] = nums[2] + nums[0]
	}

	for i := 3; i < lenNums; i++ {
		if maxMoney[i-2] > maxMoney[i-3] {
			maxMoney[i] = nums[i] + maxMoney[i-2]
		} else {
			maxMoney[i] = nums[i] + maxMoney[i-3]
		}

	}

	max := 0
	for i := lenNums; i < lenNums; i++ {
		if maxMoney[i] > max {
			max = maxMoney[i]
		}
	}

	return max
}

func main() {
	output := rob([]int{2, 3, 4, 2})
	fmt.Println(output)

	output = rob([]int{1, 6, 8, 2, 3, 4})
	fmt.Println(output)

}

输出

6
13

房屋抢劫问题的程序

原文:techbyexample.com/program-for-house-robber-problem/

概述

附近有几所房屋。每所房屋里都有一些钱。这些房屋用一个数组表示,数组中的每个条目表示该房屋里的钱数。

例如,如果我们有以下数组

[2, 3, 4, 2]

然后

  • 第一所房屋有2

  • 第二所房屋有3

  • 第三所房屋有4

  • 第四所房屋有2

盗贼可以抢劫任意数量的房屋,但不能抢劫两个相邻的房屋。例如,他可以在以下组合中抢劫上述数组中的房屋

  • 1 和 3

  • 1 和 4

  • 2 和 4

上述组合中没有相邻的房屋。问题是找出可以为盗贼带来最大收益的组合。

例如,在上述情况下,第一个组合(1 和 3)将给盗贼带来最大的收益,即 2+4=6,因此盗贼可以抢劫第一和第三所房屋,即 2+4=6

另一个例子

Input: [1, 6, 8, 2, 3, 4]
Output: 13

盗贼可以抢劫第一、第三和第六所房屋,即 1+8+4=13

这是一个动态规划问题,因为它具有最优子结构。假设这个数组的名字是money

  • dp[0] = money[0]

  • dp[1] = max(money[0], money[1])

  • dp[2] = max(money[0] + money[1], money[2])

  • dp[i] = dp[i] + max(dp[i-1], dp[i-1])

其中 dp[i] 表示如果包括第 i 所房屋,盗贼可以抢到的钱数。最后,我们返回 dp 数组中的最大值

程序

这是相应的程序。

package main

import "fmt"

func rob(nums []int) int {
	lenNums := len(nums)

	if lenNums == 0 {
		return 0
	}

	maxMoney := make([]int, lenNums)

	maxMoney[0] = nums[0]

	if lenNums > 1 {
		maxMoney[1] = nums[1]
	}

	if lenNums > 2 {
		maxMoney[2] = nums[2] + nums[0]
	}

	for i := 3; i < lenNums; i++ {
		if maxMoney[i-2] > maxMoney[i-3] {
			maxMoney[i] = nums[i] + maxMoney[i-2]
		} else {
			maxMoney[i] = nums[i] + maxMoney[i-3]
		}

	}

	max := 0
	for i := lenNums; i < lenNums; i++ {
		if maxMoney[i] > max {
			max = maxMoney[i]
		}
	}

	return max
}

func main() {
	output := rob([]int{2, 3, 4, 2})
	fmt.Println(output)

	output = rob([]int{1, 6, 8, 2, 3, 4})
	fmt.Println(output)

}

输出

6
13

字符串或数组的不同(唯一)排列

原文:techbyexample.com/unique-permutations/

概述

给定一个可以包含重复元素的整数数组,只找到所有不同的排列。

示例

Input: [2, 2, 1]
Output: [[2 2 1] [2 1 2] [1 2 2]]

Input: [2, 2, 1, 1]
Output: [[2 2 1 1] [2 1 2 1] [2 1 1 2] [2 1 1 2] [2 1 2 1] [1 2 2 1] [1 2 1 2] [1 1 2 2] [1 2 1 2] [1 2 2 1] [1 1 2 2]]

程序

下面是相应的程序。

package main

import "fmt"

func permuteUnique(nums []int) [][]int {
	return permuteUtil(nums, 0, len(nums), len(nums))
}

func shouldSwap(nums []int, start, index int) bool {
	for i := start; i < index; i++ {
		if nums[start] == nums[index] {
			return false
		}

	}
	return true

}
func permuteUtil(nums []int, start, end int, length int) [][]int {
	output := make([][]int, 0)
	if start == end-1 {
		return [][]int{nums}
	} else {
		for i := start; i < end; i++ {
			if shouldSwap(nums, start, i) {
				nums[start], nums[i] = nums[i], nums[start]
				n := make([]int, length)
				for k := 0; k < length; k++ {
					n[k] = nums[k]
				}
				o := permuteUtil(n, start+1, end, length)
				output = append(output, o...)
				nums[i], nums[start] = nums[start], nums[i]
			}

		}
	}
	return output
}

func main() {
	output := permuteUnique([]int{2, 2, 1})
	fmt.Println(output)

	output = permuteUnique([]int{2, 2, 1, 1})
	fmt.Println(output)
}

输出

[[2 2 1] [2 1 2] [1 2 2]]
[[2 2 1 1] [2 1 2 1] [2 1 1 2] [2 1 1 2] [2 1 2 1] [1 2 2 1] [1 2 1 2] [1 1 2 2] [1 2 1 2] [1 2 2 1] [1 1 2 2]]

一组句子中的最大单词数

原文:techbyexample.com/maximum-words-group-sentences/

概述

给定一组句子。找出在该组句子中出现最多单词的句子。

示例

Input: ["Hello World", "This is hat]
Output: 3

Input: ["Hello World", "This is hat", "The cat is brown"]
Output: 4

程序

下面是相应的程序。

package main

import "fmt"

func mostWordsFound(sentences []string) int {
	lenSentences := len(sentences)

	max := 0
	for i := 0; i < lenSentences; i++ {
		countWord := countW(sentences[i])
		if countWord > max {
			max = countWord
		}
	}

	return max
}

func countW(s string) int {

	lenS := len(s)
	numWords := 0

	for i := 0; i < lenS; {
		for i < lenS && string(s[i]) == " " {
			i++
		}

		if i < lenS {
			numWords++
		}

		for i < lenS && string(s[i]) != " " {
			i++
		}
	}

	return numWords
}

func main() {
	output := mostWordsFound([]string{"Hello World", "This is hat"})
	fmt.Println(output)

	output = mostWordsFound([]string{"Hello World", "This is hat", "The cat is brown"})
	fmt.Println(output)
}

输出

3
4

计算句子中单词总数的程序

原文:techbyexample.com/number-words-sentence-golang/

概述

给定一个句子,计算其中的单词数。句子中的每个单词仅包含英文字符

示例

Input: "Hello World"
Output: 2

Input: "This is hat"
Output: 3

程序

下面是相应的程序。

package main

import "fmt"

func countW(s string) int {

	lenS := len(s)
	numWords := 0

	for i := 0; i < lenS; {
		for i < lenS && string(s[i]) == " " {
			i++
		}

		if i < lenS {
			numWords++
		}

		for i < lenS && string(s[i]) != " " {
			i++
		}
	}

	return numWords
}

func main() {
	output := countW("Hello World")
	fmt.Println(output)

	output = countW("This is hat")
	fmt.Println(output)
}

输出

2
3

数列中的第 n 位数字程序

原文:techbyexample.com/nth-digit-sequence-program/

概述

给定一个整数 n,找出无限序列 {1, 2, 3, 4 ….. 无限} 中的第 n 位数字。

示例

Input: 14
Output: 1

数列 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12 … 的第 14 位是 1,它是数字 12 的一部分。

示例 2

Input: 17
Output: 3

数列 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13 … 的第 17 位是 3,它是数字 13 的一部分。

程序

下面是相应的程序。

package main

import "fmt"

func findNthDigit(n int) int {

	numDigits := 1
	tenIncrement := 1

	start := 1
	counter := 9 * tenIncrement * numDigits

	for n > counter {
		n = n - counter
		tenIncrement = tenIncrement * 10
		numDigits++
		start = start * 10
		counter = 9 * tenIncrement * numDigits
	}

	return findNthDigitUtil(start, numDigits, n)

}

func findNthDigitUtil(start, numDigits, n int) int {
	position := n % numDigits
	digitWhichHasN := 0
	if position == 0 {
		digitWhichHasN = start - 1 + n/numDigits
		return digitWhichHasN % 10
	} else {
		digitWhichHasN = start + n/numDigits
		positionFromBehind := numDigits - position
		answer := 0
		for positionFromBehind >= 0 {
			answer = digitWhichHasN % 10
			digitWhichHasN = digitWhichHasN / 10
			positionFromBehind--
		}
		return answer
	}

	return 0

}

func main() {
	output := findNthDigit(14)
	fmt.Println(output)

	output = findNthDigit(17)
	fmt.Println(output)
} 

输出

1
3

找出所有长度大于二的算术序列

原文:techbyexample.com/arithmetic-series/

概述

算术序列是一个每个元素之间的差相等的序列。在这个程序中,给定了一个整数数组。目标是找到所有长度大于二的算术序列。

这个问题最好通过一个例子来理解

示例

Input: [2,3,4,5]
Output: 3

在上面的数组中,我们有三个长度大于 2 的算术切片

  • 2,3,4

  • 3,4,5

  • 2,3,4,5

这是一个动态规划问题,因为它具有最优子结构。假设数组的名字是 input

  • dp[0] = 0

  • dp[1] = 0

  • dp[2] = 1 如果 dp[2] – dp[1] == dp[1] – dp[0]

  • dp[i] = 1 如果 dp[i] – dp[i-1] == dp[i-1] – dp[i-2]

其中 dp[i] 表示从长度大于 2 到长度 i+1 的算术子序列的数量

程序

这里是相应的程序。

package main

import "fmt"

func numberOfArithmeticSlices(nums []int) int {
	lenNums := len(nums)

	if lenNums <= 2 {
		return 0
	}

	dp := make([]int, lenNums)

	dp[0] = 0

	if (nums[2] - nums[1]) == nums[1]-nums[0] {
		dp[2] = 1
	}

	for i := 3; i < lenNums; i++ {
		if nums[i]-nums[i-1] == nums[i-1]-nums[i-2] {
			dp[i] = dp[i-1] + 1

		}
	}

	output := 0
	for i := 2; i < lenNums; i++ {
		output = output + dp[i]
	}

	return output

}

func main() {
	output := numberOfArithmeticSlices([]int{2, 3, 4, 5})
	fmt.Println(output)
}

输出

3

查找数组中的所有重复项

原文:techbyexample.com/find-all-duplicates-array/

概述

给定一个数组,其中所有元素的范围在[1, n]之间,n 为数组的长度。目标是找出该数组中的所有重复项

示例

Input: [1, 2, 3, 2, 4, 3]
Output: [2, 3]

这里的思路是利用数字范围在[1, n]的事实。对于数组中的每个元素,将其索引处的值增加 n。所以

  • 获取索引处的值时,我们使用 value%n

  • 最终,如果任何索引处的值大于 2*n,那么它就是重复的。

程序

这里是相应的程序。

package main

import "fmt"

func findDuplicates(nums []int) []int {
	lenNums := len(nums)

	for i := 0; i < lenNums; i++ {
		index := (nums[i] - 1) % lenNums

		nums[index] = lenNums + nums[index]
	}

	k := 0

	for i := 0; i < lenNums; i++ {
		if nums[i] > 2*lenNums {
			nums[k] = i + 1
			k++
		}
	}

	return nums[0:k]

}

func main() {
	output := findDuplicates([]int{1, 2, 3, 2, 4, 3})
	fmt.Println(output)
}

输出

[2 3]

如果某行或某列为零,则将矩阵置为零

原文:techbyexample.com/set-matrix-zero/

概述

给定一个 m*n 的矩阵。如果某个元素为零,则将其所在的行和列设置为零

示例

输入:

1, 1, 1 
0, 1, 1 
1, 1, 1

输出:

0, 1, 1 
0, 0, 0 
0, 1, 1

我们将通过使用两个额外的数组rowSetcolumnSet,它们的长度分别为mn,来解决这个问题。我们将遍历整个矩阵并

if matrix[i][j] == 0 then
   rowSet[i] = 1
   columnSet[j] = 1

然后,如果rowSet[i]等于 1 或columnSet[j]等于 1,我们可以将matrix[i][j]设置为零

程序

这是相应的程序。

package main

import "fmt"

func setZeroes(matrix [][]int) [][]int {
	numRows := len(matrix)

	numColumns := len(matrix[0])

	rowSet := make([]int, numRows)
	columnSet := make([]int, numColumns)

	for i := 0; i < numRows; i++ {
		for j := 0; j < numColumns; j++ {
			if matrix[i][j] == 0 {
				rowSet[i] = 1
				columnSet[j] = 1
			}
		}
	}

	for i := 0; i < numRows; i++ {
		for j := 0; j < numColumns; j++ {
			if rowSet[i] == 1 || columnSet[j] == 1 {

				matrix[i][j] = 0

			}
		}
	}

	return matrix

}

func main() {
	matrix := [][]int{{1, 1, 1}, {0, 1, 1}, {1, 1, 1}}
	output := setZeroes(matrix)
	fmt.Println(output)
}

输出

[[0 1 1] [0 0 0] [0 1 1]]

乘法运算程序:两个字符串的乘法

原文:techbyexample.com/program-multiply-two-strings/

概述

编写一个程序来乘以两个字符串。

示例

Input: "12"*"12"
Output: 144

Input: "123"*"12"
Output: 1476

程序

以下是相应的程序。

package main

import (
	"fmt"
	"math"
	"strconv"
)

func multiply(num1 string, num2 string) string {

	if len(num1) > len(num2) {
		num2, num1 = num1, num2
	}

	output := 0

	k := 0

	carry := 0

	for i := len(num1) - 1; i >= 0; i-- {

		x := 0
		temp := 0
		for j := len(num2) - 1; j >= 0; j-- {

			digit1, _ := strconv.Atoi(string(num1[i]))
			digit2, _ := strconv.Atoi(string(num2[j]))

			multiply_output := digit1*digit2 + carry

			carry = multiply_output / 10

			temp = multiply_output%10*int(math.Pow(10, float64(x))) + temp
			x = x + 1
		}

		temp = carry*int(math.Pow(10, float64(x))) + temp
		carry = 0

		output = temp*int(math.Pow(10, float64(k))) + output
		k = k + 1
	}

	return strconv.Itoa(output)
}

func main() {
	output := multiply("12", "12")
	fmt.Println(output)

	output = multiply("123", "12")
	fmt.Println(output)
}

输出

144
1476

搜索插入位置程序

原文:techbyexample.com/search-insert-position-program/

概述

给定一个已排序的数组,其中包含不同的整数和一个目标值。目标是找到该目标值在数组中的插入位置。这里有两种情况

  • 如果目标值存在于给定数组中,则返回该索引。

  • 如果目标值在给定数组中不存在,返回它应该插入的位置

示例

Input: [1,2,3]
Target Value: 4
Output: 3

Input: [1,2,3]
Target Value: 3
Output: 2

程序

这是该程序的实现。

package main

import "fmt"

func searchInsert(nums []int, target int) int {
	lenNums := len(nums)

	index := -1

	if target <= nums[0] {
		return 0
	}

	if target > nums[lenNums-1] {
		return lenNums
	}

	for i := 0; i < lenNums; i++ {
		if target <= nums[i] {
			index = i
			break
		}
	}

	return index

}

func main() {
	pos := searchInsert([]int{1, 2, 3}, 4)
	fmt.Println(pos)

	pos = searchInsert([]int{1, 2, 3}, 3)
	fmt.Println(pos)
}

输出

3
2

注意: 查看我们的 Golang 高级教程。本系列教程内容详尽,我们尽力通过示例覆盖所有概念。此教程适合那些希望掌握 Golang 并深入理解其知识的人 – Golang 高级教程

如果你有兴趣了解如何在 Golang 中实现所有设计模式。如果是的话,那么这篇文章适合你 – 所有设计模式 Golang

最小路径和程序

原文:techbyexample.com/minimum-path-sum-program/

概述

有一个 m*n 的矩阵,包含非负整数。目标是找到一条从左上角到右下角的最小路径和。你只能向右或向下移动。

例如,假设我们有如下矩阵

然后,最小路径和如下图所示。它的和为 1+1+2+2+1 = 7

[{0,0}, {1,0}, {1,1}, {2,1}, {2,2}

这是一个动态规划问题,因为它具有最优子结构。假设矩阵的名称是 input

  • minPath[0][0] = input[0][0]

  • minPath[i][j] = ming(minPath[i-1][j], minPath[i][j-1])) + input[i][j]

其中 minPath[i][j] 表示从 {0,0} 到 {i,j} 的最小路径和

程序

以下是相应的程序。

package main

import "fmt"

func minPathSum(grid [][]int) int {
	rows := len(grid)
	columns := len(grid[0])
	sums := make([][]int, rows)

	for i := 0; i < rows; i++ {
		sums[i] = make([]int, columns)
	}

	sums[0][0] = grid[0][0]

	for i := 1; i < rows; i++ {
		sums[i][0] = grid[i][0] + sums[i-1][0]
	}

	for i := 1; i < columns; i++ {
		sums[0][i] = grid[0][i] + sums[0][i-1]
	}

	for i := 1; i < rows; i++ {
		for j := 1; j < columns; j++ {
			if sums[i-1][j] < sums[i][j-1] {
				sums[i][j] = grid[i][j] + sums[i-1][j]
			} else {
				sums[i][j] = grid[i][j] + sums[i][j-1]
			}
		}
	}

	return sums[rows-1][columns-1]

}

func main() {
	input := [][]int{{1, 4, 2}, {1, 3, 2}, {2, 2, 1}}
	output := minPathSum(input)
	fmt.Println(output)
}

输出

7

注意: 请查看我们的 Golang 高级教程。本系列教程内容详尽,我们尽力用示例覆盖所有概念。本教程适用于那些希望在 Golang 中获得专业知识并深入理解的人——Golang 高级教程

如果你有兴趣了解如何在 Golang 中实现所有设计模式。如果是的话,那么这篇文章适合你——Golang 中的所有设计模式

相同的二叉树程序

原文:techbyexample.com/same-binary-tree-program%EF%BF%BC/

概述

目标是判断给定的两个二叉树是否相同。以下是相同的树示例

树 1

树 2

程序

这是相同程序的代码。

package main

import (
	"fmt"
)

type TreeNode struct {
	Val   int
	Left  *TreeNode
	Right *TreeNode
}

func isSameTree(p *TreeNode, q *TreeNode) bool {
	if p == nil && q == nil {
		return true
	}

	if p == nil || q == nil {
		return false
	}

	if p.Val != q.Val {
		return false
	}

	return isSameTree(p.Left, q.Left) && isSameTree(p.Right, q.Right)
}

func main() {
	root1 := TreeNode{Val: 1}
	root1.Left = &TreeNode{Val: 2}
	root1.Left.Left = &TreeNode{Val: 4}
	root1.Right = &TreeNode{Val: 3}
	root1.Right.Left = &TreeNode{Val: 5}
	root1.Right.Right = &TreeNode{Val: 6}

	root2 := TreeNode{Val: 1}
	root2.Left = &TreeNode{Val: 2}
	root2.Left.Left = &TreeNode{Val: 4}
	root2.Right = &TreeNode{Val: 3}
	root2.Right.Left = &TreeNode{Val: 5}
	root2.Right.Right = &TreeNode{Val: 6}

	output := isSameTree(&root1, &root2)
	fmt.Println(output)

	root1 = TreeNode{Val: 1}
	root1.Left = &TreeNode{Val: 2}

	root2 = TreeNode{Val: 1}
	root2.Left = &TreeNode{Val: 3}

	output = isSameTree(&root1, &root2)
	fmt.Println(output)
}

输出

true
false

设计一个像 bit.ly 的 URL 缩短服务

原文:techbyexample.com/url-shortner-system-design/

目录

  • 概述

  • API 详情

  • 短 URL 如何重定向到长 URL

  • 编码方案

    • MD5 方法

    • 计数器方法

    • 密钥范围

  • 高级设计

  • 短 URL 服务

    • 使用什么数据库
  • 密钥生成服务

    • 数据库架构

    • 使用什么数据库

    • 如何解决并发问题

    • 如何恢复 key_prefix

    • 如果密钥范围用尽会发生什么

    • 如果短 URL 永不过期会怎样

    • KGS 服务不是单点故障吗?

  • 其他常见组件

  • 非功能性需求

    • 可扩展性

    • 低延迟

    • 可用性

    • 告警和监控

    • 靠近用户位置

    • 避免单点故障

  • 结论

概述

URL 缩短服务用于缩短一个长的 URL。短 URL 的长度不会依赖于实际 URL 的长度。

请注意 bit.ly 中的 URL 第二部分。它将始终是固定长度,无论输入 URL 的长度如何。我们来记录下系统的功能需求

  • 无论输入 URL 的长度如何,你都应该能够生成固定长度的短 URL。

  • 一旦点击了短网址,它应该能够将用户重定向到实际的网址。

  • 每个网址应该有一个过期时间。之后,短网址将过期

  • 我们还将考虑一种情况,在本教程中,短网址永不过期

以下是一些非功能需求

  • 系统应该具有高可用性

  • 系统应该具有强一致性。这意味着,一旦为给定长网址生成了短网址,那么在下次调用时,系统应该能够根据短网址返回该长网址。

  • 系统应该具有容错能力

  • 系统中不应有单点故障

短网址应该是什么样子的?在我们查看要求之前,先看一个示例网址。以下是一些要求

  • 它必须简短且唯一。最好长度为 6-8 个字符

  • 它应该只包含 URL 安全的字符。以下是 URL 安全字符

    • 小写字母 – “a-z”

    • 大写字母 – “A-Z”

    • 数字 – “0-9”

    • 点 – “.”

    • 与符号 – “~”

    • 下划线 – “_”

    • 连字符 – “-”

这是一个短网址的示例,它包含 7 个字符作为短字符串的一部分。

https://bit.ly/sdfse43

API 详情

以下是系统中所需的 API

  • 创建短网址 – 该 API 将根据长网址创建短网址

  • 删除短网址 – 该 API 将用于删除之前创建的短网址

  • 获取网址 – 该 API 将在给定短网址时重定向到长网址

  • 列出短网址 – 查看某个特定用户的所有短网址

短网址如何重定向到长网址

一旦我们的服务接收到短网址,它将获取该短网址对应的长网址或原始网址。这里有两种情况

  • 如果不存在,它将向浏览器返回 404 资源未找到的响应

  • 如果存在,它将向浏览器发出 HTTP 302 重定向。Location 响应头将包含长网址或原始网址。浏览器将随后重定向到长网址。这是 HTTP 协议中规定的标准重定向方式

实现短网址服务的最重要部分之一是生成短网址字符串。在查看解决方案之前,我们先看看可以用于生成短网址字符串的编码方案

编码方案

设计一个唯一的短字符来表示给定长网址有三种解决方案

  • MD5

  • 计数器

  • 密钥范围

MD5 方法

我们将有一个 MD5 函数,它会为给定的长网址返回一个唯一的字符串。MD5 字符串是 128 位的固定长度。我们可以取前 42 位来表示它为 7 个 base64 字符。每个 base64 字符占 6 位,因此 6*7=42 位。我们也可以取前 48 位来表示它为 8 个 base64 字符。如何防止冲突

如你所见,MD5 方法的最大问题是碰撞。两个长 URL 可能会产生相同的前 42 位 MD5 值。那么我们如何检测碰撞呢?我们首先对给定的长 URL 进行 MD5 运算,然后取前 42 位生成 7 个 base64 字符的字符串。接着,我们检查该字符串在数据库中是否已被使用。如果已经被使用,我们就取第 2 到 43 位进行检查。

优点

  • MD5 方法的最大优点是它实现起来非常简单。

  • 如果是一个只需要生成少量短网址的系统,并且短网址生成是异步的,那么这个方案可能会非常有效,因为碰撞较少,即使发生了碰撞,由于整体过程是异步的,我们仍然有时间处理下一个短网址。

缺点

  • 正如我们所见,这个解决方案并不具有可扩展性。如果需要在几秒钟内生成大量短网址,且发生了大量碰撞时,系统的性能可能会大幅下降。

计数器方法

正如我们已经看到的,将 base10 数字转换为 base64 字符串是可行的。计数器本质上就是一个 base10 数字。因此,如果我们有一种方法为给定的长 URL 生成唯一数字,那么我们就能够生成短网址。问题是我们如何生成这个唯一的计数器或数字。这个思路是拥有一个空闲数字范围的列表。为此,我们可以再有一个服务,通过 API 返回可以使用的空闲数字范围。

优点

  • 这是一个可扩展的选项,适用于任何流量巨大的服务。

缺点

  • 将一个 base10 数字转换为 base64 不会得到固定长度的字符串。例如,将 100,000 转换为 base64 会得到“Yag”。这可能没问题,但一个专业的短网址生成服务应该采用标准。此缺点可以通过选择产生 6 位或 7 位 Base64 字符串的数字范围来规避。

  • 每次将数字转换为 base64 时都会有一定的转换成本,尽管这种转换成本可能不会产生实质性的影响。

  • 相比于 MD5,这个方法稍微复杂一点。

密钥范围

第三种选择是使用密钥范围本身,而不是计数器范围。

优点

  • 这是一个可扩展的选项,适用于任何流量巨大的服务。

缺点

  • 相比于 MD5,这个方法稍微复杂一点。

计数器和密钥范围都是不错的选择。在本教程中,我们将考虑密钥范围的方式。

高级设计

从高层次上讲,我们来讨论一下更高层次的流程以及将会有哪些服务存在。

  • 会有一个API 网关,所有用户的请求都会先到达此网关。

  • 会有一个短网址服务。该服务负责处理所有来自用户的 API 请求。

  • 会有一个密钥生成服务,它负责生成短网址的密钥。

  • 短网址 服务将会在每次需要新密钥时调用密钥生成服务。

  • 短网址 服务耗尽所有密钥范围时,它将发布一个 Kafka/SNS 消息,指定密钥范围已耗尽。

  • 这条消息将由密钥恢复服务处理,该服务将作为一个工作进程。它会将密钥范围标记为可用,从而可以再次选择该范围。该工作进程还将从数据库中删除该范围内所有密钥的 short_url – long_url 映射。

  • 一旦创建 short_url 映射,我们将立即缓存该映射

以下是该服务的高层设计图

让我们来看看 短网址 服务和 密钥生成 服务的一些细节

短网址服务

让我们来看看短网址服务需要哪些数据库。我们需要一个 short_url 表。以下是 short_url 表中的字段

  • short_url_key

  • long_url

  • user_id

  • created

  • 已更新

使用什么数据库

由于我们没有任何 ACID 要求,所以 NoSQL 数据库适合我们使用。此外,我们可能需要保存非常大的数据,因此 NoSQL 可能更合适。这个系统将是一个写重和读重的系统。而且,这里几乎没有二级索引需求,几乎所有查询都将基于主字段 short_url_key。所以我们可以在这里使用 Cassandra 数据库

如高层图所示,短网址将负责跟踪所有生成的短网址。它将与密钥生成服务协调,以获取空闲密钥范围。每个密钥可以生成一个短网址。现在让我们看看 密钥生成服务 的设计

密钥生成服务

将有一个 KGS 服务,负责生成密钥。首先,我们来看一下每个密钥的长度应为多少。可选的长度为 6、7、8。生成密钥时只能使用 base64 URL 安全字符。因此

  • 对于 6 - 我们有 64⁶ = 687 亿种选项

  • 对于 7 - 我们有 64⁷ = ~3500 亿种选项

  • 对于 8 - 我们有 64⁸ = 万亿种选项

现在我们可以假设 687 亿条记录已经足够,因此我们可以使用 6 个字符作为密钥。现在的问题是这些数据将如何在数据库中维护。如果我们将 687 亿条记录存储到数据库中,那么可能会有太多条记录,从而浪费资源。一个选项是将密钥范围存储到数据库中。我们可以存储 64 个范围,只存储前五个字符,作为所有从该前缀生成的 64 个密钥的前缀。例如,我们有以下前缀

adcA2

然后,可以从此生成以下 64 个密钥

  • adcA2[a-z] – 26 个密钥

  • adcA2[A-Z] – 26 个密钥

  • adcA2[0-9] – 10 个密钥

  • adcA2[-_] – 2 个密钥

我们可以将这些范围存储在数据库中。所以对于 6 个字符的情况,数据库中将总共存储 64⁵ 个条目。键将由键服务按范围和批次返回给短网址服务。短网址服务随后将使用这个前缀生成 64 个键,并处理 64 个不同的创建短网址请求。

这是优化,因为短网址服务只需要在它已经用完所有 64 个键时才调用键生成服务。因此,短网址服务将发出一次调用到键生成服务,用于生成 64 个短网址。

现在让我们来看一下 KGS 服务的要点。

  • 数据库架构

  • 使用哪个数据库

  • 如何解决并发问题

  • 如何恢复 key_prefix

  • 如果键范围被耗尽了,会发生什么情况

  • 如果短网址永不过期怎么办

  • KGS 服务难道不是单点故障吗?

数据库架构

只会有一个表来存储键的范围,即前缀。下表将包含该表中的字段。

  • key_prefix

  • key_length – 目前它将始终为 6。如果在任何情况下我们需要 7 位长度的键,这些字段会存在。

  • 已使用 – 如果为真,则表示该键前缀当前正在使用。如果为假,则表示它可以被使用。

  • 已创建

  • 更新

使用哪个数据库

我们没有 ACID 要求,因此可以使用 No SQL 数据库。此外,我们可能需要保存非常大的数据,所以 No SQL 数据库可能更适合。这个系统将是一个既写重也读重的系统。因此,我们可以在这里使用Cassandra 数据库。我们可以做数据库的容量估算,并基于此来决定我们希望拥有的分片数量。每个分片都会被适当的复制。

这里我们还可以进行一次优化,以提高延迟。我们可以在缓存中重新填充空闲的键范围,KGS 服务可以直接从中获取,而不是每次都访问数据库。

如何解决并发问题

很有可能会发生两个请求看到相同的前缀或范围是空闲的。由于多个服务器同时从键数据库读取,我们可能会遇到两台或更多服务器从键数据库读取到相同的空闲键范围。我们刚才提到的并发问题有两种方式可以解决。

  • 两台或更多服务器读取相同的键,但只有一台服务器能够在数据库中将该key_prefix标记为已使用。并发控制在数据库级别,也就是说每一行在更新之前都会被锁定,我们可以在这里利用这一点。数据库会返回给服务器是否有记录被更新。如果记录没有被更新,则服务器可以获取一个新的键。如果记录被更新,那么该服务器就获得了正确的键。

  • 另一种选择是使用事务,在一个事务中进行查找和更新。每次查找和更新都会返回一个唯一的 key_prefix。这可能不是一个推荐的选择,因为它会给数据库带来负载。

如何恢复 key_prefix

一旦 Tiny URL 服务用尽了密钥范围,它将把该范围输入到另一个表中,经过两周后,密钥将被恢复并重新作为免费密钥提供。我们可以确定,在两周后,密钥将是免费的,因为我们设置了两周的到期时间。

如果密钥范围用尽会发生什么?

这将是一个意外的情况。将有一个后台工作者检查密钥范围是否用尽。如果是,它可以生成 7 位长度的密钥范围。但是,如何知道密钥范围是否用尽呢?为了保持大致的计数,可能会有另一个表格来存储已使用密钥的用户计数。

  • 每当 KGS 为 Tiny URL 服务分配一个范围时,它会发布一条消息,该消息将被一个同步工作者拾取,工作者将减少已使用密钥的数量。

  • 同样,每当一个范围变为空时,我们可以递增该计数器。

如果短网址永不过期会怎样?

扩展上述服务以支持永不过期的 URL 是很容易的。

  • 只不过我们的短字符串将不再局限于 6 个字符。根据需要,我们可以使用 7 位、8 位甚至 9 位长度的字符。

  • 不会提供密钥恢复服务

  • 一旦密钥范围被分配,我们可以将其从密钥数据库中移除,因为它不再需要释放或恢复。

KGS 服务不是单点故障吗?

为了防止这种情况,我们将对密钥数据库进行适当的复制。此外,服务本身还将有多个应用服务器。我们还会设置适当的自动扩展机制。我们还可以实施灾难管理。

其他常见组件

其他常见组件可能包括:

  • 用户服务——存储用户的个人信息。

  • 令牌/认证服务——用于管理用户令牌。

  • 短信服务——用于向用户发送任何类型的消息。例如:一次性密码(OTP)

  • 分析服务——这可以用于跟踪任何类型的分析数据。

非功能性需求

现在让我们讨论一些非功能性需求。

可扩展性

上述设计中需要考虑的第一件事是可扩展性因素。系统中每个组件的可扩展性非常重要。以下是可能遇到的可扩展性挑战及其可能的解决方案。

  • 短网址服务和KGS服务中的每台机器只能处理有限数量的请求。因此,每个服务应设置适当的自动扩展机制,以便根据请求数量,我们可以增加实例并在需要时自动扩展。

  • 你的 Kafka/SNS 系统可能无法承受这么大的负载。我们可以进行水平扩展,但也有一个限制。如果这成为瓶颈,那么根据地理位置或用户 ID,我们可以设置两个或更多这样的系统。可以使用服务发现来确定请求应该发送到哪个 Kafka 系统。

  • 可扩展性的另一个重要因素是,我们设计了系统的方式,使得没有任何服务被过多的任务拖慢。我们有关注点的分离,在服务责任过重的地方,我们将其拆分开来。

低延迟

  • 我们可以缓存创建的短网址,并设置一定的过期时间。每当一个短网址被创建时,它很可能会在一段时间内被访问。这将减少许多读取请求的延迟。

  • 我们还创建了键或键范围的批次。这防止了短网址服务每次都调用 KGS 服务,从而整体上提高了延迟。

  • 还有一个优化可以改善延迟。我们可以在缓存中重新填充空闲的键范围,KGS 服务可以直接从那里获取,而不是每次都访问数据库。

可用性

为了使系统具有高可用性,几乎所有组件都必须具有冗余/备份。以下是需要执行的一些事项。

  • 在我们的数据库中,我们需要启用复制功能。每个主分片节点应该有多个从节点。

  • 对于 Redis,我们也需要启用复制。

  • 对于数据冗余,我们也可以采用多区域部署。如果其中一个区域发生故障,这可以作为一个优势。

  • 灾难恢复也可以设置

警报与监控

警报与监控也是非常重要的非功能性需求。我们应当监控我们的每一个服务并设置合适的警报。可以监控的一些内容包括:

  • API 响应时间

  • 内存使用情况

  • CPU 使用情况

  • 磁盘空间使用情况

  • 队列长度

  • ….

靠近用户位置

这里有几种架构可以选择,其中之一是单元架构。你可以在这里阅读更多关于单元架构的信息 – github.com/wso2/reference-architecture/blob/master/reference-architecture-cell-based.md

避免单点故障

单点故障是指当系统的某一部分停止工作时,会导致整个系统的故障。我们在设计时应尽量避免单点故障。通过冗余和多区域部署,我们可以防止这种情况的发生。

总结

这就是 URL 缩短服务的系统设计。希望你喜欢这篇文章。请在评论中提供反馈。

理解 Base64 编码/解码

原文:techbyexample.com/understanding-base64-encoding-decoding/

概述

Base64 也称为 Base64 内容传输编码。Base64 是将二进制数据编码为 ASCII 文本。但是它只使用 64 个字符和一个填充字符,这些字符在大多数字符集中都有。因此,它是一种仅使用可打印字符表示二进制数据的方式。这些可打印字符包括:

  • 小写字母 a-z

  • 大写字母 A-Z

  • 数字 0-9

  • 字符 +/

  • = 被用作填充字符

Base64 编码用于通过不正确处理二进制数据的媒介传输数据。因此,进行 Base64 编码是为了确保数据在通过这种媒介传输时保持完整,不被修改。Base64 编码后的数据如下所示:

OWRjNGJjMDY5MzVmNGViMGExZTdkMzNjOGMwZTI3ZWI==

为什么需要 Base64 编码

让我们谈谈历史,了解为什么需要 Base64。最初,当邮件系统开始时,只有文本可以通过电子邮件传输。后来,电子邮件开始支持附件,包括视频和音频。视频和音频是二进制数据,当二进制数据通过互联网传输时,很有可能发生损坏。那么问题是,为什么会这样?一些媒介如电子邮件只支持文本数据,它们仅用于流式传输文本,因此这些协议可能会将你的二进制数据解释为控制字符,或者某些特殊字符在二进制数据中可能会被不同地解释。二进制数据的问题在于,二进制数据中的某些字符在某些媒介中可能具有特殊含义。换句话说,这些媒介不是 8 位干净的,只能处理文本数据。

所以,Base64 是为不支持二进制数据并且仅处理文本字符的媒介设计的。因为可以保证数据不会被损坏,所以它主要用于仅支持 ASCII 数据的遗留系统。

但为什么是 64 个字符,而不是更多呢?这 64 个字符在大多数字符集中都有,因此你可以合理地相信,数据在传输到另一端时不会被损坏。

请注意,这是编码而不是加密。编码和加密有什么区别?

编码

编码意味着将数据转换为另一种格式,以便系统能够正确地处理这种转换后的格式。编码只是一个公开的算法,没有涉及密钥,并且是可逆的。因此,任何知道该算法的人都可以简单地反转并获取原始数据。现在的问题是,为什么需要它?某些传输媒介只理解文本数据或某些字符数量。你的二进制数据无法通过这种媒介传输,因为有数据损坏的风险。

编码不提供任何安全性,而仅仅是为了在理解不同格式的不同介质之间的兼容性

加密

加密是为了保密。它总是通过密钥保护,只有知道密钥的人才能解密。因此,在加密的情况下,数据只能由持有密钥的人逆向解码。

例如,通过网络发送密码。在这种情况下,我们加密密码,以便任何未授权的人无法读取它。例如 HTTPS

所以从本质上讲,编码是为了兼容性,而不是为了加密。

Base64 编码/解码的工作原理

Base64 编码在 tools.ietf.org/html/rfc4648 中有描述

Base64 编码将把每 3 个字节的数据转换成 4 个编码字符。它将从左到右开始扫描,然后选择前 3 个字节的数据,表示 3 个字符。这 3 个字节将是 24 位。现在它将把这 24 位分成 4 部分,每部分 6 位。然后,每个 6 位组将在下面的表格中索引,以获取映射的字符。

 Value Encoding  Value Encoding  Value Encoding  Value Encoding
         0 A            17 R            34 i            51 z
         1 B            18 S            35 j            52 0
         2 C            19 T            36 k            53 1
         3 D            20 U            37 l            54 2
         4 E            21 V            38 m            55 3
         5 F            22 W            39 n            56 4
         6 G            23 X            40 o            57 5
         7 H            24 Y            41 p            58 6
         8 I            25 Z            42 q            59 7
         9 J            26 a            43 r            60 8
        10 K            27 b            44 s            61 9
        11 L            28 c            45 t            62 +
        12 M            29 d            46 u            63 /
        13 N            30 e            47 v
        14 O            31 f            48 w         (pad) =
        15 P            32 g            49 x
        16 Q            33 h            50 y

让我们通过一个例子来理解。假设我们有以下字符串

ab@

上述字符串的位表示为

 a          b        @
01100001  01100010  01000000

这 24 个位将被分成 4 组,每组 6 位

011000 010110 001001 000000

上述位的数字表示为

 24    22     9      0
011000 010110 001001 000000

使用上述数字索引到 base64 表格。以下是映射结果

24 Y
22 W
9 J
0 A

所以 base64 编码后的字符串将是

YWJA

如果输入字符串不是 3 的倍数怎么办?在这种情况下,填充字符 = 将会出现

假设输入字符串有 4 个字符

ab@c

上述字符串的位表示为

 a          b        @        c
01100001  01100010  01000000 01100011

前三个字节将被组合在一起。最后的字节将用 4 个额外的零进行填充,以使得整体的位数能被 6 整除。

011000 010110 001001 000000 011000 110000
  24     22     9      0      24     48

使用上述数字索引到上面的表格。以下是映射结果

24 Y
22 W
9 J
0 A
24 Y
48 w

这将变为

YWJAYw==

每两个额外的零由 = 字符表示。由于我们添加了 4 个额外的零,因此末尾有两个 =。

现在让我们看看另一个例子,其中输入字符串有 5 个字符

ab@cd

上述字符串的位表示为

 a          b        @        c       d
01100001  01100010  01000000 01100011 01100100

前三个字节将被组合在一起。最后两个字节将组合在一起,并用 2 个额外的零进行填充,以使得整体的位数能被 6 整除。

011000 010110 001001 000000 011000 110110 010000
  24     22     9      0      24     54     16

使用上述数字索引到上面的表格。以下是映射结果

24 Y
22 W
9 J
0 A
24 Y
54 2
16 Q

这将变为

YWJAY2Q=

每两个额外的零由 = 字符表示。由于我们添加了 2 个额外的零,因此末尾有一个 =。另外,请注意,每个带有填充的 base64 编码字符串长度是四的倍数

实际字符串 Base64 编码 字符串 长度
ab@ YWJA 4
ab@c YWJAYw== 8
ab@cd YWJAY2Q= 8

对于编码,我们只有我们上面讨论的三种情况

  • 输入字符串的位数能被 6 整除。无需添加填充。例如 ab@

  • 输入字符串的位数不能被 6 整除,余数为 4. 将添加两个 == 填充。例如 ab@c

  • 输入的位数不能被 6 整除,余数为 2. 将添加一个 = 填充。例如 ab@cd

现在浮现的问题是,填充是否必要?答案是,这取决于情况。我们将在查看了解码过程后讨论这一点。

Base64 解码

现在让我们将 base64 编码的字符串解码回原始字符串。解码是编码的反向过程。我们以这个例子为例

YWJAY2Q=

将其分成多个 4 字符一组。

YWJA 

Y2Q=

现在从每组中去掉结尾的 = 字符。对于剩余的字符串,将其转换为上述表格中对应的位表示。

 Y      W       J      A     
011000 010110 001001 000000

 Y      2      Q
011000 110110 010000

现在将其分成 8 位一组。保留结尾的零。这是为了处理添加的结尾 =。结尾的零将是 00 或 0000。

01100001  01100010  01000000 

01100011 01100100

现在对每个字节,按照 ASCII 表分配相应的字符

 01100001  01100010  01000000
   a          b        @ 

01100011 01100100
  c       d

因此,最终的字符串将是

ab@cd

为什么我们要将 Base64 编码的字符串分成 4 个字符一组进行解码?原因是填充,下一节会解释清楚。

填充是否必要

你可能会好奇,base64 编码是否必须使用 = 填充,因为我们在解码时直接丢弃了填充。答案是,这取决于情况

  • 发送单一字符串时,填充并不必要。

  • 当你拼接多个字符串的 base64 编码时,填充非常重要。如果拼接的是没有填充的字符串,那么原始字符串就无法恢复,因为有关添加字节的信息会丢失。举个例子,见下文

实际字符串 带填充的 Base64 编码 不带填充的 Base64 编码
a YQ== YQ
bc YmM= YmM
def ZGVm ZGVm

现在让我们考虑这两种情况。

当拼接不带填充的数据发送时

在这种情况下,拼接后的 Base64 字符串将是

YQYmMZGVm

尝试解码它,你会得到如下的最终字符串,但它是错误的

a&1

当拼接带有填充的数据发送时

在这种情况下,拼接后的 Base64 字符串将是

YQ==YmM=ZGVm

尝试按 4 个字符一组进行解码,你会得到如下正确的最终字符串

abcdef

现在,脑海中再次浮现的问题是,为什么需要拼接多个 base64 编码的字符串?答案是在处理流式数据时,如果你希望在数据到达时直接发送 base64 编码数据,拼接是非常有用的。例如,视频缓冲。

所以这就是为什么填充是被鼓励的,尽管在所有情况下并不是绝对必要的。

大小

由于 Base64 本质上将 3 个字节编码为 4 个 ASCII 字符(如果有填充)。这四个 ASCII 字符会以每个 1 字节的形式通过网络发送。因此,结果的大小将始终比原始大小多出 33.33%。所以,如果原始字符串的大小是 n 字节,那么 Base64 编码后的大小将是:

n*4/3

Base64 替代方案

还有许多其他的编码选项,但其中一些非常复杂,而另一些则占用太多空间。例如,Base80 占用更少的空间,但却显著更难使用,因为二的幂是二进制的自然基数。另外,还有十六进制编码。它简单但占用更多空间。

  • 十六进制 – 它的字符集由 16 个字符组成。

  • Base36 – 大小写不敏感的编码。

  • Base80

  • Base58

还有其他替代方案。

Base64 其他实现

还有两种其他的 base64 实现。

  • 用于 URL 的 Base64。在这种情况下,‘+’‘\’ 被替换为‘+’‘-‘。这是因为 ‘+’‘\’ 会进一步通过 URL 编码转化为十六进制序列,从而进一步增加 URL 的长度。例如,‘+’ 会被转换为‘%2B’,‘\’ 会被编码为‘%2F’。

  • 用于文件名的 Base64。在这种情况下,‘\’ 被替换为‘-‘。这是因为 ‘\’ 在 Unix 和 Windows 的文件路径中都被使用。

Base64 应用

  • 在电子邮件中传输二进制数据,例如将视频和音频作为电子邮件附件发送。

  • 基本认证在 HTTP 协议中是作为 Base64 编码发送的。

还有其他 Base64 的应用。

结论

这就是关于 Base64 编码的所有内容。希望你喜欢这篇文章。请在评论中分享你的反馈。

计算字符串中最后一个单词长度的程序

原文:techbyexample.com/program-length-last-word/

概述

目标是找到给定字符串中最后一个单词的长度

示例

Input: "computer science"
Output: 7

The last word is science and its length is 7

Input: "computer science is a subject "
Output: 7

The last word is subject and ts length is 7

Input: " "
Output: 0

There is no last word hence answer is zero

程序

下面是实现该功能的程序。

package main

import "fmt"

func lengthOfLastWord(s string) int {
	lenS := len(s)

	lenLastWord := 0
	for i := lenS - 1; i >= 0; {
		for i >= 0 && string(s[i]) == " " {
			i--
		}
		if i < 0 {
			return 0
		}

		for i >= 0 && string(s[i]) != " " {
			//fmt.Println(i)
			//fmt.Println(string(s[i]))
			i--
			lenLastWord++
		}

		return lenLastWord
	}

	return 0
}

func main() {
	length := lengthOfLastWord("computer science")
	fmt.Println(length)

	length = lengthOfLastWord("computer science is a subject")
	fmt.Println(length)

	length = lengthOfLastWord("  ")
	fmt.Println(length)
}

输出

7
7
0

加一程序或向整数数组中加一

原文:techbyexample.com/plus-one-program/

概览

给定一个整数数组。整体上,这个整数数组表示一个数字。假设该整数数组名为 digits,那么 digits[i]表示该整数的第 i 位数字。目标是将 1 加到这个整数数组中。必须在不将数组转换为整数类型 int 的情况下完成此操作。

示例

Input: [1, 2]
Output: [1, 3]

Input: [9, 9]
Output: [1, 0, 0]

程序

下面是该程序的代码。

package main

import "fmt"

func plusOne(digits []int) []int {

	lenDigits := len(digits)

	output := make([]int, 0)

	lastDigit := digits[lenDigits-1]

	add := lastDigit + 1
	carry := add / 10

	lastDigit = add % 10
	output = append(output, lastDigit)

	for i := lenDigits - 2; i >= 0; i-- {
		o := digits[i] + carry
		lastDigit = o % 10
		carry = o / 10
		output = append(output, lastDigit)
	}

	if carry == 1 {
		output = append(output, 1)
	}

	return reverse(output, len(output))
}

func reverse(input []int, length int) []int {
	start := 0
	end := length - 1

	for start < end {
		input[start], input[end] = input[end], input[start]
		start++
		end--
	}

	return input
}

func main() {
	output := plusOne([]int{1, 2})
	fmt.Println(output)

	output = plusOne([]int{9, 9})
	fmt.Println(output)
}

输出

[1 3]
[1 0 0]

分区链表的程序

原文:techbyexample.com/program-partition-linked-list/

概述

给定一个链表,并且给定一个目标值。将链表进行分区,使得所有小于目标值的节点排在大于目标值的节点之前。

示例

Input: 4->5->3->1
Output: 2
Target: 1->4->5->3

原始顺序应保持不变

程序

以下是相应的程序。

package main

import "fmt"

func main() {
	first := initList()
	first.AddFront(1)
	first.AddFront(2)
	first.AddFront(3)
	first.AddFront(4)

	first.Head.Traverse()
	newHead := partition(first.Head, 2)
	fmt.Println("")
	newHead.Traverse()

}

func initList() *SingleList {
	return &SingleList{}
}

type ListNode struct {
	Val  int
	Next *ListNode
}

func (l *ListNode) Traverse() {
	for l != nil {
		fmt.Println(l.Val)
		l = l.Next
	}
}

type SingleList struct {
	Len  int
	Head *ListNode
}

func (s *SingleList) AddFront(num int) {
	ele := &ListNode{
		Val: num,
	}
	if s.Head == nil {
		s.Head = ele
	} else {
		ele.Next = s.Head
		s.Head = ele
	}
	s.Len++
}

func partition(head *ListNode, x int) *ListNode {
	if head == nil {
		return nil
	}

	curr := head

	var prev *ListNode

	for curr != nil {
		if curr.Val >= x {
			break
		}
		prev = curr
		curr = curr.Next
	}

	if curr == nil {
		return head
	}

	firstLargeValueNode := curr

	prev2 := firstLargeValueNode
	for curr != nil {
		if curr.Val < x {
			prev2.Next = curr.Next
			if prev != nil {
				prev.Next = curr
				prev = prev.Next
				prev.Next = firstLargeValueNode
			} else {
				if head == firstLargeValueNode {
					head = curr
				}
				curr.Next = firstLargeValueNode
				prev = curr
			}
		}
		prev2 = curr
		curr = curr.Next
	}

	return head
}

输出

4
5
3
1

1
4
5
3

最佳买卖股票时间程序

原文:techbyexample.com/best-time-buy-sell-stocks-program/

概述

给定一个数组prices,其中prices[i]表示第 i 天的股票价格。你只能进行一次买卖。找到通过一次买卖股票所能获得的最大利润。

示例

Input: [4,2,3,8,1]
Output: 6
Buy on the second day at 2 and sell on the 4th day at 8\. Profit = 8-2 = 6

原始顺序应保持不变。

程序

这是相应的程序。

package main

import "fmt"

func maxProfit(prices []int) int {
	lenPrices := len(prices)

	buy := -1
	sell := -1

	maxProphit := 0

	for i := 0; i < lenPrices; {
		for i+1 < lenPrices && prices[i] > prices[i+1] {
			i++
		}

		if i == lenPrices-1 {
			return maxProphit
		}

		buy = i

		i++

		for i+1 < lenPrices && prices[i] < prices[i+1] {
			i++
		}

		sell = i

		if (prices[sell] - prices[buy]) > maxProphit {
			maxProphit = prices[sell] - prices[buy]
		}
		i++
	}

	return maxProphit
}

func main() {
	output := maxProfit([]int{4, 2, 3, 8, 1})
	fmt.Println(output)
}

输出

6

唯一路径程序

原文:techbyexample.com/unique-paths-program/

概述

有一个 m*n 的网格。机器人位于位置 (0,0)。机器人只能向右或向下移动。那么机器人到达右下角 (m-1, n-1) 的总路径数是多少?

示例

Input: m=2 , n=2
Output: 2

Robot can reach the right down corner in two ways. 
1\. [0,0] -> [0,1]-> [1, 1]
2\. [0,0] -> [1,0]-> [1, 1]

这个程序还有另一种变体,其中网格中的某个位置可能包含障碍物。让我们先来看第一种变体,稍后再介绍第二种变体。

第一种变体

我们将通过动态规划来解决这个问题

  • 创建一个大小为 m*n 的路径矩阵

  • paths[i][j] 表示机器人到达 (i,j) 位置的路径数

  • paths[0][0] = 0

  • paths[i][j] = paths[i-1][j] + paths[i][j-1]

程序

下面是相应的程序代码。

package main

import "fmt"

func uniquePaths(m int, n int) int {
	paths := make([][]int, m)

	for i := 0; i < m; i++ {
		paths[i] = make([]int, n)
	}

	paths[0][0] = 1

	for i := 1; i < m; i++ {
		paths[i][0] = 1
	}

	for i := 1; i < n; i++ {
		paths[0][i] = 1
	}

	for i := 1; i < m; i++ {
		for j := 1; j < n; j++ {
			paths[i][j] = paths[i-1][j] + paths[i][j-1]
		}
	}

	return paths[m-1][n-1]
}

func main() {
	output := uniquePaths(3, 7)
	fmt.Println(output)
}

输出

6

第二种变体我们也将通过动态规划来解决这个问题

  • 创建一个大小为 m*n 的路径矩阵

  • paths[i][j] 表示机器人到达 (i,j) 位置的路径数

  • paths[0][0] = 0

  • 如果paths[i][j]不是障碍物,则paths[i][j] = paths[i-1][j] + paths[i][j-1]

  • 如果paths[i][j]是障碍物,则paths[i][j] = 0

程序

package main

import "fmt"

func uniquePathsWithObstacles(obstacleGrid [][]int) int {

	m := len(obstacleGrid)
	n := len(obstacleGrid[0])
	paths := make([][]int, len(obstacleGrid))

	for i := 0; i < m; i++ {
		paths[i] = make([]int, n)
	}

	if obstacleGrid[0][0] != 1 {
		paths[0][0] = 1
	}

	for i := 1; i < m; i++ {
		if obstacleGrid[i][0] == 1 {
			break
		} else {
			paths[i][0] = paths[i-1][0]
		}

	}

	for i := 1; i < n; i++ {
		if obstacleGrid[0][i] == 1 {
			break
		} else {
			paths[0][i] = paths[0][i-1]
		}

	}

	for i := 1; i < m; i++ {
		for j := 1; j < n; j++ {
			if obstacleGrid[i][j] != 1 {
				paths[i][j] = paths[i-1][j] + paths[i][j-1]
			}

		}
	}

	return paths[m-1][n-1]
}

func main() {
	output := uniquePathsWithObstacles([][]int{{0, 0, 0}, {0, 1, 0}, {0, 0, 0}})
	fmt.Println(output)
}

输出

2

跳跃游戏程序

原文:techbyexample.com/jump-game-program/

概述

提供了一个输入数组。数组中的每个条目表示从该位置开始的最大跳跃长度。要求从第一个索引开始,如果能到达最后一个索引则返回 true,如果无法到达最后一个索引则返回 false。

示例

Input: [2, 3, 1, 1, 4]
Output: true

Input: [3, 2, 1, 0, 4]
Output: false

在第一个示例中,有多种方式可以到达最后一个索引。

  • 0-1-4

  • 0-2-3-4

在第二个示例中,没有办法到达最后一个索引。你能到达的最远位置是倒数第二个索引。由于倒数第二个索引的值为零,因此无法到达最后一个索引。

还有一种变种是需要返回最小跳跃次数。稍后我们将查看该程序。

程序

package main

import "fmt"

func canJump(nums []int) bool {
	lenNums := len(nums)
	canJumpB := make([]bool, lenNums)

	canJumpB[0] = true

	for i := 0; i < lenNums; i++ {

		if canJumpB[i] {
			valAtCurrIndex := nums[i]
			for k := 1; k <= valAtCurrIndex && i+k < lenNums; k++ {
				canJumpB[i+k] = true
			}
		}

	}

	return canJumpB[lenNums-1]
}

func main() {
	input := []int{2, 3, 1, 1, 4}

	canJumpOrNot := canJump(input)
	fmt.Println(canJumpOrNot)

	input = []int{3, 2, 1, 0, 4}

	canJumpOrNot = canJump(input)
	fmt.Println(canJumpOrNot)

}

输出

true
false

还有一种变种是需要返回最小跳跃次数。下面是对应的程序。

package main

import "fmt"

func jump(nums []int) int {

	lenJump := len(nums)
	minJumps := make([]int, lenJump)
	for i := 0; i < lenJump; i++ {
		minJumps[i] = -1
	}

	minJumps[0] = 0

	for i := 0; i < lenJump; i++ {
		currVal := nums[i]

		for j := 1; j <= currVal && i+j < lenJump; j++ {
			if minJumps[i+j] == -1 || minJumps[i+j] > minJumps[i]+1 {
				minJumps[i+j] = minJumps[i] + 1
			}
		}
	}

	return minJumps[lenJump-1]

}

func main() {
	input := []int{2, 3, 1, 1, 4}

	minJump := jump(input)
	fmt.Println(minJump)

	input = []int{3, 2, 1, 0, 4}

	minJump = jump(input)
	fmt.Println(minJump)

}

输出

2
-1

在数组中就地移除给定值的所有出现

原文:techbyexample.com/remove-element/

概述

给定一个整数数组和一个目标元素。将数组中所有该目标元素的出现移除。移除操作必须在原地完成。

Input: [1, 4, 2, 5, 4]
Target: 4
Output: [1, 2, 5]

Input: [1, 2, 3]
Target:3
Output: [1, 2]

程序

这是相应的程序。

package main

import (
	"fmt"
)

func removeElement(nums []int, val int) []int {
	lenNums := len(nums)

	k := 0

	for i := 0; i < lenNums; {
		if nums[i] != val {
			nums[k] = nums[i]
			k++
		}
		i++
	}
	return nums[0:k]
}

func main() {
	output := removeElement([]int{1, 4, 2, 5, 4}, 4)
	fmt.Println(output)

	output = removeElement([]int{1, 2, 3}, 3)
	fmt.Println(output)
}

输出

[1 2 5]
[1 2]

合并两个已排序数组的程序

原文:techbyexample.com/program-merge-two-sorted-arrays/

概述

给定两个数组。两个数组都已经排序。

  • 第一个数组的长度为 m+n

  • 第二个数组的长度为 n

目标是合并这两个已排序的数组。第一个数组具有足够的长度,因此只应修改第一个数组。

Input1: [2,3,4,0,0]
Input2: [1,5]
Output: [1, 2, 3, 4, 5]

Input1: [4,5,0,0,0,0]
Input2: [1, 2, 3, 7]
Output: [1, 2, 3, 4, 5, 7]

这里是我们可以采取的方法

  • 将第一个数组中的所有元素按排序顺序移到末尾。第一个数组将变成
[0,0,2,3,4]
  • 现在从第一个数组的mth索引元素和第二个数组的0th索引开始。

  • 比较两个数组,并将较小的元素放置在第一个数组的0th索引位置。第一个数组将变成

[1, 0, 2, 3, 4]
  • 重复这个过程。第一个数组末尾的值将在被覆盖之前移到前面,因为我们有足够的空间。

程序

这里是实现该功能的程序。

package main

import "fmt"

func merge(nums1 []int, m int, nums2 []int, n int) []int {

	if m == 0 {
		for k := 0; k < n; k++ {
			nums1[k] = nums2[k]
		}
		return nums1
	}
	nums1 = moveToEnd(nums1, m)
	i := n
	j := 0
	for k := 0; k < m+n; k++ {
		if i < m+n && j < n {
			if nums1[i] < nums2[j] {
				nums1[k] = nums1[i]
				i++
			} else {
				nums1[k] = nums2[j]
				j++
			}
		} else if j < n {
			nums1[k] = nums2[j]
			j++
		}

	}

	return nums1

}

func moveToEnd(nums []int, m int) []int {
	lenNums := len(nums)

	k := lenNums

	for i := m - 1; i >= 0; i-- {
		nums[k-1] = nums[i]
		k--
	}

	return nums
}

func main() {
	output := merge([]int{2, 3, 4, 0, 0}, 3, []int{1, 5}, 2)
	fmt.Println(output)

	output = merge([]int{4, 5, 0, 0, 0, 0}, 2, []int{1, 2, 3, 7}, 4)
	fmt.Println(output)
}

输出

[1 2 3 4 5]
[1 2 3 4 5 7]

给定整数数组的幂集程序

原文:techbyexample.com/program-for-power-set-array/

概述

给定一个包含所有唯一元素的整数数组。目标是返回该数组的幂集

Input: [1, 2]
Output: [[],[1],[2],[1,2]]

Input: [1, 2, 3]
Output: [[] [1] [2] [1 2] [3] [1 3] [2 3] [1 2 3]]

如果给定数组中的元素个数为 n,那么幂集中的元素个数将是 pow(2, n)。假设 n 是 3,那么幂集中的元素个数将是 pow(2, n)=8

假设我们取从 0 到(8-1)之间的所有二进制转换,即从 0 到 7。

000
001
010
011
100
101
110
111

上面每个二进制数字代表一个幂集

例如

000 - []
001 - [1]
010 - [2]
011 - [1, 2]
100 - [3]
101 - [1, 3]
110 - [2, 3]
111 - [1, 2, 3]

程序

这是相应的程序。

package main

import (
	"fmt"
	"math"
)

func subsets(nums []int) [][]int {

	lengthNums := len(nums)
	powerSetLength := int(math.Pow(2, float64(lengthNums)))
	output := make([][]int, 0)

	for i := 0; i < powerSetLength; i++ {

		result := make([]int, 0)
		for j := 0; j < lengthNums; j++ {
			val := int(i) & int(1<<j)
			if val != 0 {
				result = append(result, nums[j])
			}
		}

		output = append(output, result)
	}

	return output
}

func main() {
	output := subsets([]int{1, 2})
	fmt.Println(output)

	output = subsets([]int{1, 2, 3})
	fmt.Println(output)
}

输出

[[] [1] [2] [1 2]]
[[] [1] [2] [1 2] [3] [1 3] [2 3] [1 2 3]]

统计数字序列转换为字母的所有可能解码

原文:techbyexample.com/count-possible-decodings-digit/

概述

假设我们有下面的数字到字母的映射

'A' -> "1"
'B' -> "2"
...
'Z' -> "26"

目标是统计给定数字序列的所有可能解码方式

示例

Input: '15'
Output: 2

15 can be decoded as "AE" or "O"

程序

这是相应的程序。

package main

import (
	"fmt"
	"strconv"
)

func numDecodings(s string) int {

	runeArray := []rune(s)
	length := len(runeArray)
	if length == 0 {
		return 0
	}

	if string(runeArray[0]) == "0" {
		return 0
	}

	numwaysArray := make([]int, length)

	numwaysArray[0] = 1

	if length == 1 {
		return numwaysArray[0]
	}

	secondDigit := string(runeArray[0:2])
	num, _ := strconv.Atoi(secondDigit)
	if num > 10 && num <= 19 {
		numwaysArray[1] = 2
	} else if num > 20 && num <= 26 {
		numwaysArray[1] = 2
	} else if num == 10 || num == 20 {
		numwaysArray[1] = 1
	} else if num%10 == 0 {
		numwaysArray[1] = 0
	} else {
		numwaysArray[1] = 1
	}

	for i := 2; i < length; i++ {
		firstDigit := string(runeArray[i])
		if firstDigit != "0" {
			numwaysArray[i] = numwaysArray[i] + numwaysArray[i-1]
		}

		secondDigit := string(runeArray[i-1 : i+1])
		fmt.Println(i)
		fmt.Println(secondDigit)

		num, _ := strconv.Atoi(secondDigit)

		if num >= 10 && num <= 26 {
			numwaysArray[i] = numwaysArray[i] + numwaysArray[i-2]
		}
	}

	return numwaysArray[length-1]

}

func main() {
	output := numDecodings("15")
	fmt.Println(output)
}

输出

2

电话号码字母组合程序

原文:techbyexample.com/letter-combinations-phone/

概述

给定一个包含一些数字的输入字符串。数字与字母的映射方式类似于电话键盘上的映射

2 = either "a", "b" or "c"
3 = either "d", "e" or "f"
4 = either "g", "h" or "i"
5 = either "j", "k" or "l"
6 = either "m", "n" or "co"
7 = either "p", "q" "r" or "s"
8 = either "t", "u" or "v"
9 = either "w", "x", "y" or "z"

给定一个只包含数字的输入字符串。目标是返回所有字母组合。

示例

Input: "3"
Output: [d e f]

Input: "34"
Output: [dg dh di eg eh ei fg fh fi]

Input: "345"
Output: [dgj dgk dgl dhj dhk dhl dij dik dil egj egk egl ehj ehk ehl eij eik eil fgj fgk fgl fhj fhk fhl fij fik fil]

程序

下面是相应的程序。

package main

import "fmt"

func letterCombinations(digits string) []string {
	if digits == "" {
		return nil
	}
	letterMap := make(map[string][]string)

	letterMap["2"] = []string{"a", "b", "c"}
	letterMap["3"] = []string{"d", "e", "f"}
	letterMap["4"] = []string{"g", "h", "i"}
	letterMap["5"] = []string{"j", "k", "l"}
	letterMap["6"] = []string{"m", "n", "o"}
	letterMap["7"] = []string{"p", "q", "r", "s"}
	letterMap["8"] = []string{"t", "u", "v"}
	letterMap["9"] = []string{"w", "x", "y", "z"}

	runeDigits := []rune(digits)
	length := len(runeDigits)

	temp := ""

	return letterCombinationsUtil(runeDigits, 0, length, temp, letterMap)

}

func letterCombinationsUtil(runeDigits []rune, start, length int, temp string, letterMap map[string][]string) []string {

	if start == length {
		return []string{temp}
	}

	currentDigit := string(runeDigits[start])

	letters := letterMap[currentDigit]

	final := make([]string, 0)
	for _, val := range letters {
		t := temp + val
		output := letterCombinationsUtil(runeDigits, start+1, length, t, letterMap)
		final = append(final, output...)
	}
	return final
}

func main() {

	output := letterCombinations("3")
	fmt.Println(output)

	output = letterCombinations("34")
	fmt.Println(output)

	output = letterCombinations("345")
	fmt.Println(output)
}

输出

[d e f]
[dg dh di eg eh ei fg fh fi]
[dgj dgk dgl dhj dhk dhl dij dik dil egj egk egl ehj ehk ehl eij eik eil fgj fgk fgl fhj fhk fhl fij fik fil]

句子中的单词反转

原文:techbyexample.com/reverse-words-sentence/

概述

目标是反转给定句子中的单词

示例

Input: "hello world"
Output: "word hello"

另一个例子。如果输入仅包含一个单词,那么该单词将被返回。

Input: "hello"
Output: "hello"

这里是策略

  • 首先,我们反转整个字符串。所以对于“hello world”,它变成了
"dlrow olleh"
  • 然后我们反转每个单词
"world hello"
  • 我们还需要注意去除开头或结尾的多余空格。

程序

这是相同的程序。

package main

import (
	"fmt"
	"regexp"
	"strings"
)

func reverseWords(s string) string {

	runeArray := []rune(s)
	length := len(runeArray)

	reverseRuneArray := reverse(runeArray)

	for i := 0; i < length; {
		for i < length && string(reverseRuneArray[i]) == " " {
			i++
		}
		if i == length {
			break
		}
		wordStart := i

		for i < length && string(reverseRuneArray[i]) != " " {
			i++
		}

		wordEnd := i - 1

		reverseRuneArray = reverseIndex(reverseRuneArray, wordStart, wordEnd)

	}

	noSpaceString := strings.TrimSpace(string(reverseRuneArray))
	space := regexp.MustCompile(`\s+`)
	return space.ReplaceAllString(noSpaceString, " ")
}

func reverse(s []rune) []rune {
	length := len(s)
	start := 0
	end := length - 1
	for start < end {
		s[start], s[end] = s[end], s[start]
		start++
		end--
	}
	return s
}

func reverseIndex(s []rune, i, j int) []rune {

	start := i
	end := j
	for start < end {
		s[start], s[end] = s[end], s[start]
		start++
		end--
	}
	return s
}

func main() {
	output := reverseWords("hello world")
	fmt.Println(output)

	output = reverseWords("hello")
	fmt.Println(output)
}

输出

world hello
hello

删除链表的中间节点

原文:techbyexample.com/delete-middle-node-linked-list/

概述

目标是删除链表的中间节点。如果 x 是链表的大小,那么中间节点是

mid = x/2

示例

Input: 1->2->3->4->5
Output: 1->2->4->5

程序

这里是相应的程序。

package main

import "fmt"

func main() {
	first := initList()
	first.AddFront(5)
	first.AddFront(4)
	first.AddFront(3)
	first.AddFront(2)
	first.AddFront(1)

	first.Head.Traverse()
	deleteMiddle(first.Head)
	fmt.Println("")
	first.Head.Traverse()

}

func initList() *SingleList {
	return &SingleList{}
}

type ListNode struct {
	Val  int
	Next *ListNode
}

func (l *ListNode) Traverse() {
	for l != nil {
		fmt.Println(l.Val)
		l = l.Next
	}
}

type SingleList struct {
	Len  int
	Head *ListNode
}

func (s *SingleList) AddFront(num int) {
	ele := &ListNode{
		Val: num,
	}
	if s.Head == nil {
		s.Head = ele
	} else {
		ele.Next = s.Head
		s.Head = ele
	}
	s.Len++
}

func deleteMiddle(head *ListNode) *ListNode {

	if head == nil {
		return nil
	}

	size := sizeOfList(head)

	mid := size / 2

	if mid == 0 {
		return head.Next
	}

	curr := head
	for i := 0; i < mid-1; i++ {
		curr = curr.Next
	}

	prev := curr

	midNode := prev.Next

	if midNode == nil {
		return head
	}

	midNext := midNode.Next

	prev.Next = midNext

	return head

}

func sizeOfList(head *ListNode) int {
	l := 0
	for head != nil {
		l = l + 1
		head = head.Next
	}
	return l
}

输出

1
2
3
4
5

1
2
4
5

从链表中删除倒数第 n 个节点

原文:techbyexample.com/remove-nth-node-end-linked-list/

概述

目标是从链表的末尾删除倒数第 n 个节点。如果链表的大小为x,那么倒数第 n 个节点的位置是(x-n+1)

示例

Input: 1->2->3->4->5
Node to be deleted from the end: 2
Output: 1->2->3->5

程序

这是相应的程序。

package main

import "fmt"

func main() {
	first := initList()
	first.AddFront(5)
	first.AddFront(4)
	first.AddFront(3)
	first.AddFront(2)
	first.AddFront(1)

	first.Head.Traverse()
	removeNthFromEnd(first.Head, 2)
	fmt.Println("")
	first.Head.Traverse()

}

func initList() *SingleList {
	return &SingleList{}
}

type ListNode struct {
	Val  int
	Next *ListNode
}

func (l *ListNode) Traverse() {
	for l != nil {
		fmt.Println(l.Val)
		l = l.Next
	}
}

type SingleList struct {
	Len  int
	Head *ListNode
}

func (s *SingleList) AddFront(num int) {
	ele := &ListNode{
		Val: num,
	}
	if s.Head == nil {
		s.Head = ele
	} else {
		ele.Next = s.Head
		s.Head = ele
	}
	s.Len++
}

func removeNthFromEnd(head *ListNode, n int) *ListNode {
	size := lengthOfList(head)

	numberFront := size - n + 1

	if numberFront < 1 {
		return head
	}

	if numberFront == 1 {
		return head.Next
	}

	if size == 1 {
		return nil
	}

	//Get to the numberFront-1 node
	curr := head
	for i := 0; i < numberFront-2; i++ {
		curr = curr.Next
	}

	prev := curr

	nodeToDelete := curr.Next

	if nodeToDelete != nil {
		nodeToDeleteNext := nodeToDelete.Next
		prev.Next = nodeToDeleteNext
	} else {
		prev.Next = nil
	}

	return head
}

func lengthOfList(head *ListNode) int {
	size := 0
	for head != nil {
		size = size + 1
		head = head.Next
	}
	return size
}

输出

1
2
3
4
5

1
2
3
5

组合求和问题的程序

原文:techbyexample.com/program-combination-sum-problem/

概述

给定一个整数数组和一个目标数字,目标是找到数组中所有能够和为目标数字的组合数。

数组中的每个元素可以在同一组合中使用任意次数

示例

Input: [3,4,10,11]
Target: 10
Output: [[3,3,4],[10]]

程序

下面是相应的程序。

package main

import "fmt"

func combinationSum(candidates []int, target int) [][]int {
	lengthCandidates := len(candidates)
	current_sum_array := make([]int, 0)
	output := make([][]int, 0)
	combinationSumUtil(candidates, lengthCandidates, 0, 0, 0, target, current_sum_array, &output)
	return output
}

func combinationSumUtil(candidates []int, lengthCandidates, index, current_sum_index, current_sum, target int, current_sum_array []int, output *[][]int) {

	if index >= lengthCandidates {
		return
	}

	if current_sum > target {
		return
	}

	if current_sum == target {
		var o []int
		for i := 0; i < current_sum_index; i++ {
			o = append(o, current_sum_array[i])
		}
		*output = append(*output, o)
		return
	}

	//Exclude
	combinationSumUtil(candidates, lengthCandidates, index+1, current_sum_index, current_sum, target, current_sum_array, output)

	//Include
	current_sum_array = append(current_sum_array, candidates[index])

	combinationSumUtil(candidates, lengthCandidates, index, current_sum_index+1, current_sum+candidates[index], target, current_sum_array, output)
}

func main() {
	output := combinationSum([]int{3, 4, 10, 11}, 10)
	fmt.Println(output)
}

输出

[[10] [3 3 4]]

计算一个数的幂的程序

原文:techbyexample.com/program-power-number/

概述

目标是计算给定整数的幂。将有两个输入

  • 数字本身 — 数字可以是正数也可以是负数,还可以是浮动数

  • 幂 — 幂可以是正数也可以是负数

示例

Input: Num:2, Power:4
Output: 16

Input: Num:2, Power:-4
Output: 0.0625

程序

这里是相应的程序。

package main

import "fmt"

func pow(x float64, n int) float64 {

	if x == 0 {
		return 0
	}

	if n == 0 {
		return 1
	}

	if n == 1 {
		return x
	}

	if n == -1 {
		return 1 / x
	}

	val := pow(x, n/2)

	m := x
	if n < 0 {
		m = 1 / x
	}

	if n%2 == 1 || n%2 == -1 {
		return val * val * m
	} else {
		return val * val
	}

}

func main() {
	output := pow(2, 4)
	fmt.Println(output)

	output = pow(2, -4)
	fmt.Println(output)
}

输出

16
0.0625

范围和二维数组程序

原文:techbyexample.com/range-sum-2d-array-program/

概述

给定一个二维矩阵,目标是计算矩阵中由其左上角(row1, col1)和右下角(row2, col2)定义的矩形内的元素

看起来很简单,对吧?只需从左上角遍历到右下角的给定数组并返回总和。但问题在于,允许的时间复杂度是 O(1)。

这是我们可以遵循的方法,以便能够在 O(1) 时间复杂度内返回答案

  • 预先计算另一个 sum_array 用于该二维矩阵

  • sum_array[i][j] = 从左上角(0, 0)到右下角(i, j)的数字之和。

  • 对于给定的左上角(row1, col1)和右下角(row2, col2),计算

topSum = sum_matrix[row1-1][col2]
leftSum = sum_matrix[row2][col1-1]
cornerSum = sum_matrix[row1-1][col1-1]
  • 然后返回
sum_matrix[row2][col2] - topSum - leftSum + cornerSum

程序

这里是相应的程序。

package main

import "fmt"

type NumMatrix struct {
	matrix     [][]int
	sum_matrix [][]int
	numColumn  int
	numRows    int
}

func initNumArray(matrix [][]int) NumMatrix {
	numRows := len(matrix)
	numColumn := len(matrix[0])

	if numColumn == 0 || numRows == 0 {
		return NumMatrix{}
	}

	sum_matrix := make([][]int, numRows)

	for i := 0; i < numRows; i++ {
		sum_matrix[i] = make([]int, numColumn)
	}

	sum_matrix[0][0] = matrix[0][0]
	for i := 1; i < numRows; i++ {
		sum_matrix[i][0] = matrix[i][0] + sum_matrix[i-1][0]
	}

	for i := 1; i < numColumn; i++ {
		sum_matrix[0][i] = matrix[0][i] + sum_matrix[0][i-1]
	}

	for i := 1; i < numRows; i++ {
		for j := 1; j < numColumn; j++ {
			sum_matrix[i][j] = matrix[i][j] + sum_matrix[i-1][j] + sum_matrix[i][j-1] - sum_matrix[i-1][j-1]
		}
	}

	num_matrix := NumMatrix{
		matrix:     matrix,
		sum_matrix: sum_matrix,
		numColumn:  numColumn,
		numRows:    numRows,
	}
	return num_matrix
}

func (this *NumMatrix) SumRegion(row1 int, col1 int, row2 int, col2 int) int {

	topSum := 0
	leftSum := 0
	cornerSum := 0
	if row1 > 0 {
		topSum = this.sum_matrix[row1-1][col2]
	}

	if col1 > 0 {
		leftSum = this.sum_matrix[row2][col1-1]
	}

	if row1 > 0 && col1 > 0 {
		cornerSum = this.sum_matrix[row1-1][col1-1]
	}

	return this.sum_matrix[row2][col2] - topSum - leftSum + cornerSum
}

func main() {
	matrix := [][]int{{1, 3, 5}, {6, 7, 4}}
	na := initNumArray(matrix)

	output := na.SumRegion(0, 1, 1, 2)
	fmt.Println(output)

}

输出

19

区间和数组程序

原文:techbyexample.com/range-sum-array-program/

概述

给定了一个数字数组。目标是找到该数组中的区间和。这意味着会给出一个区间,其中包含左索引和右索引。我们需要在给定的数字数组中找到左索引和右索引之间的总和。

看起来很简单,对吧?只需从给定数组的左索引遍历到右索引并返回总和。但问题在于,允许的时间复杂度是 O(1)。

这里是我们可以遵循的方法,以便能够在 O(1) 时间复杂度内返回答案。

  • 从给定的数字数组中预先计算出另一个 sum_array。

  • sum_array[i] = 从索引 0 到索引 i 的数字之和。

  • 对于给定的区间,包含左索引和右索引,只需返回 sum_array[left-1] – sum_array[right]。当然,我们需要验证 left-1 是否大于或等于零。

程序

这是相应的程序。

package main

import "fmt"

type NumArray struct {
	sum_nums []int
}

func initNumArray(nums []int) NumArray {
	length := len(nums)
	sum_nums := make([]int, length)

	sum_nums[0] = nums[0]

	for i := 1; i < length; i++ {
		sum_nums[i] = nums[i] + sum_nums[i-1]
	}

	return NumArray{
		sum_nums: sum_nums,
	}
}

func (this *NumArray) SumRange(left int, right int) int {
	leftSum := 0
	if left > 0 {
		leftSum = this.sum_nums[left-1]
	}
	return this.sum_nums[right] - leftSum
}

func main() {
	nums := []int{1, 3, 5, 6, 2}
	na := initNumArray(nums)

	output := na.SumRange(2, 4)
	fmt.Println(output)

}

输出

13

程序用于查找给定子字符串是否存在

原文:techbyexample.com/program-substring-exists/

概述

在本教程中,我们将看到一种最简单的方法来查找给定字符串中的子字符串。请注意,这可能不是最有效的策略。

策略将是

  • 匹配给定字符串中从每个索引开始的子字符串

这种方法的整体时间复杂度将是 O(mn),其中 m 是子字符串的长度,n是输入字符串的大小

我们的程序将返回给定子字符串在原始字符串中开始的索引。如果子字符串不存在于给定字符串中,程序将返回-1。

示例

Input: "lion"
Substring: "io"
Output: 1

“io”“lion” 中的位置为 1

另一个例子

“tus”“tub” 中不存在

程序

下面是相应的程序。

package main

import "fmt"

func strStr(haystack string, needle string) int {
	runeHayStack := []rune(haystack)
	runeNeedle := []rune(needle)
	lenHayStack := len(runeHayStack)
	lenNeedle := len(runeNeedle)

	if lenNeedle == 0 && lenHayStack == 0 {
		return 0
	}

	if lenNeedle > lenHayStack {
		return -1
	}

	for i := 0; i <= lenHayStack-lenNeedle; i++ {
		k := i
		j := 0

		for j < lenNeedle {
			if runeHayStack[k] == runeNeedle[j] {
				k = k + 1
				j = j + 1
			} else {
				break
			}
		}

		if j == lenNeedle {
			return i
		}
	}

	return -1

}

func main() {
	output := strStr("lion", "io")
	fmt.Println(output)

	output = strStr("tub", "tus")
	fmt.Println(output)
}

输出

1
-1

注意: 查看我们的 Golang 进阶教程。该系列教程内容详尽,我们尽力通过示例覆盖所有概念。本教程适合那些希望获得 Golang 专业知识并深入理解的人——Golang 进阶教程

如果你有兴趣了解如何在 Golang 中实现所有设计模式。如果是的话,那么这篇文章适合你——所有设计模式 Golang

排序数组转高度平衡 BST 的程序

原文:techbyexample.com/program-sorted-array-bst/

概述

给定一个排序数组,数组按升序排列。目标是将该排序数组转换为一个高度平衡的二叉搜索树。

平衡二叉搜索树(BST)是指左子树和右子树的高度差对于每个节点最大为 1 的二叉搜索树。

程序

以下是该程序。

package main

import "fmt"

type TreeNode struct {
	Val   int
	Left  *TreeNode
	Right *TreeNode
}

func sortedArrayToBST(nums []int) *TreeNode {
	length := len(nums)
	return sortedArrayToBSTUtil(nums, 0, length-1)
}

func sortedArrayToBSTUtil(nums []int, first int, last int) *TreeNode {

	if first > last {
		return nil
	}

	if first == last {
		return &TreeNode{
			Val: nums[first],
		}
	}

	mid := (first + last) / 2

	root := &TreeNode{
		Val: nums[mid],
	}

	root.Left = sortedArrayToBSTUtil(nums, first, mid-1)
	root.Right = sortedArrayToBSTUtil(nums, mid+1, last)
	return root
}

func traverseInorder(root *TreeNode) {
	if root == nil {
		return
	}

	traverseInorder(root.Left)
	fmt.Println(root.Val)
	traverseInorder(root.Right)
}

func main() {
	root := sortedArrayToBST([]int{1, 2, 3, 4, 5})
	traverseInorder(root)

}

输出

1
2
3
4
5

直方图中的最大矩形面积

原文:techbyexample.com/histogram-largest-rectangular-area/

概述

有一组每根宽度为 1 单位但高度不同的柱子并排放置。柱子的高度通过数组表示

[2, 0 , 2, 1, 3, 1]

数组表示的是

  • 柱子的总数是 5

  • 第一根柱子的高度是 2

  • 第二根柱子的高度是 0

  • 第三根柱子的高度是 2

  • 第四根柱子的高度是 1

  • 第五根柱子的高度是 3

  • 第六根柱子的高度是 1

目标是找到直方图中的最大矩形面积。从图示可以看到,最大矩形面积是 4。

以下是我们可以采取的方法来解决此问题。我们将使用栈并在每个柱子的索引处找到矩形面积,假设该柱子完全包含在最大矩形中。

  • 将给定数组的第一个元素推入栈中。遍历给定的数组。对于每根柱子,我们需要找到左侧最近的较小柱子和右侧最近的较小柱子

  • 对于当前元素,检查栈顶元素的高度是否大于当前元素的高度

  • 如果是,那么当前元素就是右侧最近的较小柱子。栈顶元素之后的元素是左侧最近的较小柱子。

  • 弹出该元素并计算最大矩形面积,假设该柱子完全包含在矩形内。跟踪最大矩形面积

  • 重复上述两个步骤,直到栈为空或栈顶元素的高度小于当前元素

  • 将当前元素推入栈中

  • 最后返回最大矩形面积。

程序

以下是相应的程序。

package main

import "fmt"

type customStack struct {
	stack []int
}

func (c *customStack) Push(num int) {
	c.stack = append(c.stack, num)
}

func (c *customStack) Pop() (int, error) {
	length := len(c.stack)
	poppedItem := 0
	if length > 0 {
		poppedItem = c.stack[length-1]
		c.stack = c.stack[:length-1]
		return poppedItem, nil
	}
	return 0, fmt.Errorf("stack is empty")
}

func (c *customStack) Front() (int, error) {
	length := len(c.stack)
	if length > 0 {
		return c.stack[length-1], nil
	}
	return 0, fmt.Errorf("stack is empty")
}

func (c *customStack) Size() int {
	return len(c.stack)
}

func largestRectangleArea(heights []int) int {
	customStack := &customStack{}

	lenHeights := len(heights)

	customStack.Push(0)

	maxRectangleSize := heights[0]

	for i := 1; i < lenHeights; i++ {

		for customStack.Size() != 0 {
			current, _ := customStack.Front()
			if heights[current] > heights[i] {
				var rectangleUsingCurrentBar int
				current, _ := customStack.Pop()
				//Calcualte max rectangle using the current front
				previous, err := customStack.Front()
				if err != nil {
					previous = -1
				}
				rectangleUsingCurrentBar = (i - previous - 1) * heights[current]
				if rectangleUsingCurrentBar > maxRectangleSize {
					maxRectangleSize = rectangleUsingCurrentBar
				}
			} else {
				break
			}
		}
		customStack.Push(i)
	}

	front, err := customStack.Front()
	if err != nil {
		return maxRectangleSize
	}
	var rectangleUsingCurrentBar int
	for customStack.Size() != 0 {
		current, _ := customStack.Pop()
		previous, err := customStack.Front()
		if err != nil {
			previous = -1
		}
		rectangleUsingCurrentBar = (front - previous) * heights[current]
		if rectangleUsingCurrentBar > maxRectangleSize {
			maxRectangleSize = rectangleUsingCurrentBar
		}
	}
	return maxRectangleSize
}

func main() {
	output := largestRectangleArea([]int{2, 0, 2, 1, 3, 1})
	fmt.Println(output)
} 

输出

4

检测链表中循环起始节点的程序

原文:techbyexample.com/program-detect-cycle-linked-list/

概述

目标是找出给定链表中的循环起始节点。如果链表中的最后一个节点指向前面的某个节点,则链表中存在循环。

示例

上面的链表有一个循环,循环的起始节点是节点 2。下面是我们可以遵循的方法。

  • 首先,检测给定的链表是否有循环。使用两个指针,一个是慢指针,另一个是快指针。两者最初都指向头节点。

  • 现在,慢指针每次移动 1 个节点,快指针每次移动 2 个节点。

slow := slow.Next
fast := fast.Next.Next
  • 如果慢指针和快指针在任何时刻相同,则说明链表中有循环。

  • 快指针和慢指针只能在循环内的某个节点相遇。假设它们在节点 3 相遇。现在,获取循环的长度。长度是 3。

  • 然后,将一个指针保持在头节点,另一个指针与它保持与循环长度相等的距离。所以,一个指针将位于节点 1,另一个指针将位于节点 4。

  • 移动两个指针,直到它们相同。它们将在循环的起始节点相遇,该节点是节点 2。

程序

以下是相应的程序。

package main

import "fmt"

func main() {
	first := initList()
	ele4 := first.AddFront(4)
	first.AddFront(3)
	ele2 := first.AddFront(2)
	first.AddFront(1)

	//Create cycle
	ele4.Next = ele2

	output := cycleStartNode(first.Head)
	fmt.Println(output.Val)

}

type ListNode struct {
	Val  int
	Next *ListNode
}

type SingleList struct {
	Len  int
	Head *ListNode
}

func (s *SingleList) AddFront(num int) *ListNode {
	ele := &ListNode{
		Val: num,
	}
	if s.Head == nil {
		s.Head = ele
	} else {
		ele.Next = s.Head
		s.Head = ele
	}
	s.Len++
	return ele
}

func initList() *SingleList {
	return &SingleList{}
}
func cycleStartNode(head *ListNode) *ListNode {
	if head == nil || head.Next == nil {
		return nil
	}

	slow := head
	fast := head

	cycleExists := false

	for slow != nil && fast != nil && fast.Next != nil {
		slow = slow.Next
		fast = fast.Next.Next

		if slow == fast {
			cycleExists = true
			break
		}
	}

	if !cycleExists {
		return nil
	}

	cycleNode := slow

	curr := cycleNode

	lengthCycle := 1

	for curr.Next != cycleNode {
		lengthCycle++
		curr = curr.Next
	}

	curr = head

	for i := 0; i < lengthCycle; i++ {
		curr = curr.Next
	}

	for head != curr {
		head = head.Next
		curr = curr.Next
	}

	return head
}

输出

2

程序:旋转链表

原文:techbyexample.com/program-to-rotate-linked-list/

概述

目标是将给定的链表按照指定的位数朝正确的方向旋转。

示例

Input: 1->2->3->4->5
k: 2

Output: 4->5->1->2->3

程序

下面是相应的程序。

package main

import "fmt"

func main() {
	first := initList()
	first.AddFront(5)
	first.AddFront(4)
	first.AddFront(3)
	first.AddFront(2)
	first.AddFront(1)

	first.Head.Traverse()

	head := rotateRight(first.Head, 2)
	fmt.Println("")
	head.Traverse()
	fmt.Println("")

}

func initList() *SingleList {
	return &SingleList{}
}

type ListNode struct {
	Val  int
	Next *ListNode
}

func (l *ListNode) Traverse() {
	for l != nil {
		fmt.Print(l.Val)
		l = l.Next
	}
}

type SingleList struct {
	Len  int
	Head *ListNode
}

func (s *SingleList) Reverse() {

	curr := s.Head
	var prev *ListNode
	var next *ListNode

	for curr != nil {
		next = curr.Next
		curr.Next = prev
		prev = curr
		curr = next
	}
	s.Head = prev
}
func (s *SingleList) AddFront(num int) {
	ele := &ListNode{
		Val: num,
	}
	if s.Head == nil {
		s.Head = ele
	} else {
		ele.Next = s.Head
		s.Head = ele
	}
	s.Len++
}

func rotateRight(head *ListNode, k int) *ListNode {

	if head == nil {
		return nil
	}

	if k == 0 {
		return head
	}

	lenList := 0

	curr := head
	var last *ListNode

	for curr != nil {
		lenList++
		last = curr
		curr = curr.Next
	}

	k = k % lenList
	if k == 0 {
		return head
	}

	curr = head

	newHeadIndex := lenList - k

	var prev *ListNode

	for i := 0; i < newHeadIndex; i++ {
		prev = curr
		curr = curr.Next
	}

	newHeadNode := prev.Next

	prev.Next = nil

	last.Next = head
	return newHeadNode
}

输出

12345
45123

注意: 查看我们的 Golang 高级教程。本系列教程内容详尽,我们尽力通过示例覆盖所有概念。本教程适合那些希望掌握并深入理解 Golang 的人——Golang 高级教程

如果你有兴趣了解所有设计模式如何在 Golang 中实现。如果是,那么这篇文章适合你——所有设计模式 Golang

用于检查给定链表是否有环的程序

原文:techbyexample.com/program-linked-list-cycle/

概述

目标是检查给定的链表是否有环。如果链表中的最后一个节点指向链表中前面的某个节点,那么链表中就存在环。

示例

上面的链表有环。下面是我们可以遵循的方法。

  • 有两个指针,一个是慢指针,另一个是快指针。两者最初都指向头节点。

  • 现在将慢指针移动一个节点,快指针移动两个节点。

slow := slow.Next
fast := fast.Next.Next
  • 如果慢指针和快指针在任何时刻相同,则链表中存在环。

程序

下面是相应的程序。

package main

import "fmt"

func main() {
	first := initList()
	ele4 := first.AddFront(4)
	first.AddFront(3)
	ele2 := first.AddFront(2)
	first.AddFront(1)

	//Create cycle
	ele4.Next = ele2

	output := hasCycle(first.Head)
	fmt.Println(output)

}

type ListNode struct {
	Val  int
	Next *ListNode
}

type SingleList struct {
	Len  int
	Head *ListNode
}

func (s *SingleList) AddFront(num int) *ListNode {
	ele := &ListNode{
		Val: num,
	}
	if s.Head == nil {
		s.Head = ele
	} else {
		ele.Next = s.Head
		s.Head = ele
	}
	s.Len++
	return ele
}

func initList() *SingleList {
	return &SingleList{}
}
func hasCycle(head *ListNode) bool {

	if head == nil || head.Next == nil {
		return false
	}

	hasCycle := false
	slow := head
	fast := head

	for slow != nil && fast != nil && fast.Next != nil {
		slow = slow.Next
		fast = fast.Next.Next

		if slow == fast {
			hasCycle = true
			break
		}
	}

	return hasCycle

}

输出

true

最长递增子序列长度的程序

原文:techbyexample.com/longest-increasing-subsequence/

概述

目标是找到给定输入数组中的最长递增子序列。它是一个给定序列中的最长子序列,且每个元素都大于其前一个元素

例如

Input: [1,5,7,6]
The longest subsequence is [1,5,6] which is of length 3
Output: 3

另一个例子

Input: [3,2,1]
The longest subsequence is either {3}, {2} or {1}. Each is of length 1
Output: 1

最长递增子序列是一个动态规划问题。假设输入数组名为input。假设lis是一个数组,其中lis[i]表示索引i处的最长递增子序列的长度。

然后

  • lis[0] = 1

  • lis[i] = max(lis[j]) + 1,其中 0 <= j < iinput[i] > input[j]

  • 如果没有这样的j,则lis[i] = 1

程序

这是相同问题的程序。

package main

import "fmt"

func lengthOfLIS(nums []int) int {

	lenNums := len(nums)
	lis := make([]int, lenNums)

	for i := 0; i < lenNums; i++ {
		lis[i] = 1
	}

	for i := 1; i < lenNums; i++ {
		for j := 0; j < i; j++ {
			if nums[i] > nums[j] && lis[i] < (lis[j]+1) {
				lis[i] = lis[j] + 1
			}
		}
	}

	max := 0

	for i := 0; i < lenNums; i++ {
		if lis[i] > max {
			max = lis[i]
		}
	}

	return max
}

func main() {
	output := lengthOfLIS([]int{1, 5, 7, 6})
	fmt.Println(output)

	output = lengthOfLIS([]int{3, 2, 1})
	fmt.Println(output)
}

输出

3
1

检查给定的两个字符串是否是字谜

原文:techbyexample.com/anagrams-two-string/

概述

字谜 是通过重新排列一个单词或短语中的字母,通常使用原始字母且每个字母只使用一次,形成的一个新单词或短语。例如,单词 anagram 本身可以重新排列成 nagaram,同样,单词 binary 可以变成 brainy^,而单词 adobe 可以变成 abode

例如

Input: abc, bac
Ouput: true

Input: abc, cb
Ouput: false

这是实现这个的思路。创建一个字符串到整数的映射。现在

  • 遍历第一个字符串,并增加映射中每个字符的计数

  • 遍历第二个字符串,并减少映射中每个字符的计数

  • 再次遍历第一个字符串,如果映射中某个字符的计数不为零,则返回 false。

  • 最后返回 true

程序

这是相应的程序代码。

package main

import "fmt"

func isAnagram(s string, t string) bool {

	lenS := len(s)
	lenT := len(t)

	if lenS != lenT {
		return false
	}

	anagramMap := make(map[string]int)

	for i := 0; i < lenS; i++ {
		anagramMap[string(s[i])]++
	}

	for i := 0; i < lenT; i++ {
		anagramMap[string(t[i])]--
	}

	for i := 0; i < lenS; i++ {
		if anagramMap[string(s[i])] != 0 {
			return false
		}
	}

	return true
}

func main() {
	output := isAnagram("abc", "bac")
	fmt.Println(output)

	output = isAnagram("abc", "bc")
	fmt.Println(output)
}

输出

true
false

程序:在不使用乘法或除法运算符的情况下,除以两个整数

原文:techbyexample.com/divide-two-integers-progam/

概述

给定两个数字,目标是将这两个数字相除并返回商。在解法中忽略余数。但是必须在不使用乘法或除法运算符的情况下进行除法运算。

  • 第一个数字是被除数

  • 第二个数字是除数

例如

Input: 15,2
Ouput: 7

Input: -15,2
Ouput: -7

Input: 15,-2
Ouput: -7

Input: -15,-2
Ouput: 7

这里是实现方法的思路。首先要注意的是

  • 如果被除数和除数都是正数或都是负数,则商是正数

  • 如果被除数和除数中有一个是负数,则商是负数

因此,被除数和除数的符号之间有一个异或关系。我们可以按照以下步骤编写程序

  • 首先,根据上面的异或逻辑确定商的符号。

  • 然后将被除数和除数都变为正数。

  • 现在将除数加倍,直到它小于或等于被除数。同时,为每次增量保持一个计数器

  • counter*sign 将是答案

程序

这是相应的程序。

package main

import (
	"fmt"
	"math"
)

func divide(dividend int, divisor int) int {

	sign := 1
	if dividend < 0 || divisor < 0 {
		sign = -1
	}

	if dividend < 0 && divisor < 0 {
		sign = 1
	}

	if dividend < 0 {
		dividend = -1 * dividend
	}

	if divisor < 0 {
		divisor = -1 * divisor
	}

	start := divisor

	i := 0

	for start <= dividend {
		start = start + divisor
		i++
	}

	output := i * sign

	return output
}

func main() {
	output := divide(15, 2)
	fmt.Println(output)

	output = divide(-15, 2)
	fmt.Println(output)

	output = divide(15, -2)
	fmt.Println(output)

	output = divide(-15, -2)
	fmt.Println(output)
}

输出

7
-7
-7
7

求一个数字的各位数字之和程序

原文:techbyexample.com/add-all-digits-number-program/

概述

目标是反复将一个数字的所有数字相加,直到结果只剩一个数字。

例如

Input: 453
Step 1: 4+5+3 = 12
Step 2: 1+2 =3

Output: 3

另一个例子

Input: 45
Step 1: 4+5 = 9

Output: 9

程序

这是相同程序的代码

package main

import "fmt"

func addDigits(num int) int {

	if num < 10 {
		return num
	}

	for num > 9 {
		num = sum(num)
	}

	return num

}

func sum(num int) int {
	output := 0
	for num > 0 {
		output = output + num%10
		num = num / 10
	}
	return output
}

func main() {
	output := addDigits(453)
	fmt.Println(output)

	output = addDigits(45)
	fmt.Println(output)

}

输出

3
9

最大连续子数组和程序

原文:techbyexample.com/maximum-continuous-subarray-sum-program/

概述

目标是找到给定输入数组中的最大子数组。子数组应该是连续的,并且至少包含一个元素

例如

Input: [4, 5 ,-3]
Maximum Subarray is [4, 5]
Output: 9

另一个示例

Input: [1, 2, -4, 4, 1]
Maximum Subarray is [4, 1]
Output: 5

我们在这里使用 Kadane 算法。在 Kadane 算法中,我们保持两个变量maxcurrentMax

  • max初始化为 IntMin,currentMax初始化为零

  • 然后我们遍历数组中的每个元素

    • 设置currentMax = currentMax + a[i]

    • 如果currentMax > max,则将max设置为currentMax

    • 如果currentMax小于零,则currentMax被重置为零

程序

这是相同的程序

package main

import "fmt"

const (
	UintSize = 32 << (^uint(0) >> 32 & 1)
	MaxInt   = 1<<(UintSize-1) - 1 // 1<<31 - 1 or 1<<63 - 1
	MinInt   = -MaxInt - 1
)

func main() {
	input := []int{4, 5, -3}
	output := maxSubArray(input)
	fmt.Println(output)

	input = []int{1, 2, -4, 4, 1}
	output = maxSubArray(input)
	fmt.Println(output)
}

func maxSubArray(nums []int) int {
	lenNums := len(nums)

	currentMax := 0
	max := MinInt
	for i := 0; i < lenNums; i++ {
		currentMax = currentMax + nums[i]
		if currentMax > max {
			max = currentMax
		}

		if currentMax < 0 {
			currentMax = 0
		}

	}
	return max
}

输出

9
5

二叉树的中序遍历

原文:techbyexample.com/inorder-traversal-of-a-binary-tree/

概述

在二叉树的中序遍历中,我们遵循以下顺序

  • 访问左子树

  • 访问根节点

  • 访问右子树

例如,假设我们有以下二叉树

然后中序遍历结果是

[4 2 1 5 3 6]

程序

这里是相应的程序

package main

import (
	"fmt"
)

type TreeNode struct {
	Val   int
	Left  *TreeNode
	Right *TreeNode
}

func inorderTraversal(root *TreeNode) []int {
	if root == nil {
		return nil
	}

	left := inorderTraversal(root.Left)
	right := inorderTraversal(root.Right)

	output := make([]int, 0)

	output = append(output, left...)
	output = append(output, root.Val)
	output = append(output, right...)
	return output

}

func main() {
	root := TreeNode{Val: 1}
	root.Left = &TreeNode{Val: 2}
	root.Left.Left = &TreeNode{Val: 4}
	root.Right = &TreeNode{Val: 3}
	root.Right.Left = &TreeNode{Val: 5}
	root.Right.Right = &TreeNode{Val: 6}

	output := inorderTraversal(&root)
	fmt.Println(output)

}

输出

[4 2 1 5 3 6]

二叉树的先序遍历

原文:techbyexample.com/preorder-traversal-of-a-binary-tree/

概览

在二叉树的先序遍历中,我们遵循以下顺序

  • 访问根节点

  • 访问左子树

  • 访问右子树

例如,假设我们有如下的二叉树

那么先序遍历的结果将会是

[1 2 4 3 5 6]

程序

这是相应的程序

package main

import (
	"fmt"
)

type TreeNode struct {
	Val   int
	Left  *TreeNode
	Right *TreeNode
}

func preorderTraversal(root *TreeNode) []int {
	if root == nil {
		return nil
	}

	left := preorderTraversal(root.Left)
	right := preorderTraversal(root.Right)

	output := make([]int, 0)

	output = append(output, root.Val)
	output = append(output, left...)
	output = append(output, right...)
	return output
}

func main() {
	root := TreeNode{Val: 1}
	root.Left = &TreeNode{Val: 2}
	root.Left.Left = &TreeNode{Val: 4}
	root.Right = &TreeNode{Val: 3}
	root.Right.Left = &TreeNode{Val: 5}
	root.Right.Right = &TreeNode{Val: 6}

	output := preorderTraversal(&root)
	fmt.Println(output)

}

输出

[1 2 4 3 5 6]

二叉树的后序遍历

原文:techbyexample.com/postorder-traversal-binary-tree/

概述

在二叉树的后序遍历中,我们遵循以下顺序

  • 访问左子树

  • 访问右子树

  • 访问根节点

例如,假设我们有下面的二叉树

然后后序遍历将是

[4 2 5 6 3 1]

程序

这是相应的程序

package main

import (
	"fmt"
)

type TreeNode struct {
	Val   int
	Left  *TreeNode
	Right *TreeNode
}

func postorderTraversal(root *TreeNode) []int {
	if root == nil {
		return nil
	}

	left := postorderTraversal(root.Left)
	right := postorderTraversal(root.Right)

	output := make([]int, 0)

	output = append(output, left...)

	output = append(output, right...)

	output = append(output, root.Val)
	return output
}

func main() {
	root := TreeNode{Val: 1}
	root.Left = &TreeNode{Val: 2}
	root.Left.Left = &TreeNode{Val: 4}
	root.Right = &TreeNode{Val: 3}
	root.Right.Left = &TreeNode{Val: 5}
	root.Right.Right = &TreeNode{Val: 6}

	output := postorderTraversal(&root)
	fmt.Println(output)

}

输出

[4 2 5 6 3 1]

用于从已排序数组中移除重复项的程序

原文:techbyexample.com/program-remove-duplicates-sorted-array/

概述

目标是从已排序的数组中移除重复项。

示例

Input: [1, 1, 1, 2]
Output: [1, 2]

Input: [1, 2, 3, 3]
Output: [1, 2, 3]

程序

以下是相同功能的程序

package main

import "fmt"

func main() {
	input := []int{1, 1, 1, 2}
	output := removeDuplicates(input)
	fmt.Println(output)

	input = []int{1, 2, 3, 3}
	output = removeDuplicates(input)
	fmt.Println(output)
}

func removeDuplicates(nums []int) []int {
	lenArray := len(nums)

	k := 0
	for i := 0; i < lenArray; {
		nums[k] = nums[i]
		k++
		for i+1 < lenArray && nums[i] == nums[i+1] {
			i++
		}
		i++
	}

	return nums[0:k]
}

输出

[1 2]
[1 2 3]

面试问题:设计一个对象池

原文:techbyexample.com/design-object-pool/

目录

  • 概述

  • 当我们需要创建一个对象池时

  • UML 图

  • 低级设计

  • 程序

  • 完整工作代码:

  • 结论

概述

对象池设计模式可以用来设计对象池。它是一种创建型设计模式,其中对象池在预先初始化并创建好后被保存在池中。需要时,客户端可以从池中请求一个对象,使用后再将其归还到池中。池中的对象永远不会被销毁。在本教程中,我们将看到

  • 当我们需要创建一个对象池时

  • 设计的 UML 图

  • 低级设计

  • 完整的工作代码

当我们需要创建一个对象池时

  • 当类的对象创建成本较高,且在特定时间内需要的此类对象数量不多时。

-以数据库连接为例。每个连接对象的创建成本较高,因为涉及网络调用,而且一次只需要有限数量的连接。对象池设计模式非常适用于这种情况。

  • 当池对象是不可变对象时

-再以数据库连接为例。数据库连接是一个不可变的对象。几乎没有它的任何属性需要更改。

  • 出于性能考虑。这将显著提升应用程序的性能,因为池已经创建好。

UML 图

以下是对象池设计的 UML 图

这里是总体思路

  • 我们有一个Pool类,它负责管理Pool对象。Pool类首先通过一组已经创建的固定数量的池对象进行初始化。然后它支持借出、归还和移除池对象。

  • 有一个接口iPoolObject,它表示将驻留在池中的对象类型。根据使用场景的不同,这个接口会有不同的实现。例如,在数据库连接的情况下,数据库连接会实现该iPoolObject接口。

  • 有一个客户端类,它使用Pool类借用一个Pool Object。当它使用完Pool Object后,会将其归还给池。

低级设计

以下是用 Go 编程语言表示的低级设计。稍后我们还将看到一个完整的示例。

iPoolObject 接口

type iPoolObject interface {
    getID() string
    doSomething()
}

DBConnection 类

type dbconnection struct {
    id string
}

func (c *dbconnection) getID() string 

func (c *dbconnection) doSomething()

Pool 类

type pool struct {
    idle   []iPoolObject
    active []iPoolObject
    capacity int
    mulock   *sync.Mutex
}

//InitPool Initialize the pool
func initPool(poolObjects []iPoolObject) (*pool, error) {}

func (p *pool) loan() (iPoolObject, error) {}

func (p *pool) receive(target iPoolObject) error {}

func (p *pool) remove(target iPoolObject) error {}

func (p *pool) setMaxCapacity(capacity int){}

客户端类

type client struct {
    pool *pool
}

func (c *client) init() {}

func (c *client) doWork() {} 

程序

这里是完整的工作代码,如果有任何人对 Go 编程语言感兴趣的话。

iPoolObject.go

package main

type iPoolObject interface {
	getID() string //This is any id which can be used to compare two different pool objects
	doSomething()
}

dbconnection.go

package main

import "fmt"

type dbconnection struct {
	id string
}

func (c *dbconnection) getID() string {
	return c.id
}

func (c *dbconnection) doSomething() {
	fmt.Printf("Connection with id %s in action\n", c.getID())
}

pool.go

package main

import (
	"fmt"
	"sync"
)

type pool struct {
	idle     []iPoolObject
	active   []iPoolObject
	capacity int
	mulock   *sync.Mutex
}

//InitPool Initialize the pool
func initPool(poolObjects []iPoolObject) (*pool, error) {
	if len(poolObjects) == 0 {
		return nil, fmt.Errorf("Cannot craete a pool of 0 length")
	}
	active := make([]iPoolObject, 0)
	pool := &pool{
		idle:     poolObjects,
		active:   active,
		capacity: len(poolObjects),
		mulock:   new(sync.Mutex),
	}
	return pool, nil
}

func (p *pool) loan() (iPoolObject, error) {
	p.mulock.Lock()
	defer p.mulock.Unlock()
	if len(p.idle) == 0 {
		return nil, fmt.Errorf("No pool object free. Please request after sometime")
	}
	obj := p.idle[0]
	p.idle = p.idle[1:]
	p.active = append(p.active, obj)
	fmt.Printf("Loan Pool Object with ID: %s\n", obj.getID())
	return obj, nil
}

func (p *pool) receive(target iPoolObject) error {
	p.mulock.Lock()
	defer p.mulock.Unlock()
	err := p.remove(target)
	if err != nil {
		return err
	}
	p.idle = append(p.idle, target)
	fmt.Printf("Return Pool Object with ID: %s\n", target.getID())
	return nil
}

func (p *pool) remove(target iPoolObject) error {
	currentActiveLength := len(p.active)
	for i, obj := range p.active {
		if obj.getID() == target.getID() {
			p.active[currentActiveLength-1], p.active[i] = p.active[i], p.active[currentActiveLength-1]
			p.active = p.active[:currentActiveLength-1]
			return nil
		}
	}
	return fmt.Errorf("Targe pool object doesn't belong to the pool")
}

func (p *pool) setMaxCapacity(capacity int) {
	p.capacity = capacity
}

client.go

package main

import (
	"fmt"
	"log"
	"strconv"
)

type client struct {
	pool *pool
}

func (c *client) init() {
	connections := make([]iPoolObject, 0)
	for i := 0; i < 3; i++ {
		c := &dbconnection{id: strconv.Itoa(i)}
		connections = append(connections, c)
	}
	var err error
	c.pool, err = initPool(connections)
	if err != nil {
		log.Fatalf("Init Pool Error: %s", err)
	}
}

func (c *client) doWork() {
	fmt.Printf("Capacity: %d\n\n", c.pool.capacity)

	conn1, err := c.pool.loan()
	if err != nil {
		log.Fatalf("Pool Loan Error: %s", err)
	}
	conn1.doSomething()

	fmt.Printf("InUse: %d\n\n", c.pool.inUse)
	conn2, err := c.pool.loan()
	if err != nil {
		log.Fatalf("Pool Loan Error: %s", err)
	}
	conn2.doSomething()
	fmt.Printf("InUse: %d\n\n", c.pool.inUse)

	c.pool.receive(conn1)
	fmt.Printf("InUse: %d\n\n", c.pool.inUse)

	c.pool.receive(conn2)
	fmt.Printf("InUse: %d\n", c.pool.inUse)
}

main.go

package main

func main() {
    client := &client{}
    client.init()
    client.doWork()
}

输出

Capacity: 3

Loan Pool Object with ID: 0
Connection with id 0 in action
InUse: 1

Loan Pool Object with ID: 1
Connection with id 1 in action
InUse: 2

Return Pool Object with ID: 0
InUse: 1

Return Pool Object with ID: 1
InUse: 0

完整工作代码:

这里是一个文件中的完整工作代码

main.go

package main

import (
	"fmt"
	"log"
	"strconv"
	"sync"
)

type pool struct {
	idle     []iPoolObject
	active   []iPoolObject
	capacity int
	inUse    int
	mulock   *sync.Mutex
}

//InitPool Initialize the pool
func initPool(poolObjects []iPoolObject) (*pool, error) {
	if len(poolObjects) == 0 {
		return nil, fmt.Errorf("Cannot craete a pool of 0 length")
	}
	active := make([]iPoolObject, 0)
	pool := &pool{
		idle:     poolObjects,
		active:   active,
		capacity: len(poolObjects),
		mulock:   new(sync.Mutex),
	}
	return pool, nil
}

func (p *pool) loan() (iPoolObject, error) {
	p.mulock.Lock()
	defer p.mulock.Unlock()
	if len(p.idle) == 0 {
		return nil, fmt.Errorf("No pool object free. Please request after sometime")
	}
	obj := p.idle[0]
	p.idle = p.idle[1:]
	p.active = append(p.active, obj)
	p.inUse = p.inUse + 1
	fmt.Printf("Loan Pool Object with ID: %s\n", obj.getID())
	return obj, nil
}

func (p *pool) receive(target iPoolObject) error {
	p.mulock.Lock()
	defer p.mulock.Unlock()
	err := p.remove(target)
	if err != nil {
		return err
	}
	p.idle = append(p.idle, target)
	p.inUse = p.inUse - 1
	fmt.Printf("Return Pool Object with ID: %s\n", target.getID())
	return nil
}

func (p *pool) remove(target iPoolObject) error {
	currentActiveLength := len(p.active)
	for i, obj := range p.active {
		if obj.getID() == target.getID() {
			p.active[currentActiveLength-1], p.active[i] = p.active[i], p.active[currentActiveLength-1]
			p.active = p.active[:currentActiveLength-1]
			return nil
		}
	}
	return fmt.Errorf("Targe pool object doesn't belong to the pool")
}

func (p *pool) setMaxCapacity(capacity int) {
	p.capacity = capacity
}

type iPoolObject interface {
	getID() string //This is any id which can be used to compare two different pool objects
	doSomething()
}

type dbconnection struct {
	id string
}

func (c *dbconnection) getID() string {
	return c.id
}

func (c *dbconnection) doSomething() {
	fmt.Printf("Connection with id %s in action\n", c.getID())
}

type client struct {
	pool *pool
}

func (c *client) init() {
	connections := make([]iPoolObject, 0)
	for i := 0; i < 3; i++ {
		c := &dbconnection{id: strconv.Itoa(i)}
		connections = append(connections, c)
	}
	var err error
	c.pool, err = initPool(connections)
	if err != nil {
		log.Fatalf("Init Pool Error: %s", err)
	}
}

func (c *client) doWork() {
	fmt.Printf("Capacity: %d\n\n", c.pool.capacity)

	conn1, err := c.pool.loan()
	if err != nil {
		log.Fatalf("Pool Loan Error: %s", err)
	}
	conn1.doSomething()

	fmt.Printf("InUse: %d\n\n", c.pool.inUse)
	conn2, err := c.pool.loan()
	if err != nil {
		log.Fatalf("Pool Loan Error: %s", err)
	}
	conn2.doSomething()
	fmt.Printf("InUse: %d\n\n", c.pool.inUse)

	c.pool.receive(conn1)
	fmt.Printf("InUse: %d\n\n", c.pool.inUse)

	c.pool.receive(conn2)
	fmt.Printf("InUse: %d\n", c.pool.inUse)
}

func main() {
	client := &client{}
	client.init()
	client.doWork()
}

输出

Capacity: 3

Loan Pool Object with ID: 0
Connection with id 0 in action
InUse: 1

Loan Pool Object with ID: 1
Connection with id 1 in action
InUse: 2

Return Pool Object with ID: 0
InUse: 1

Return Pool Object with ID: 1
InUse: 0

结论

这篇文章主要介绍了对象池的设计。希望你喜欢这篇文章。请在评论中分享反馈

posted @ 2024-11-22 16:56  绝不原创的飞龙  阅读(99)  评论(0)    收藏  举报