书接前文。
在前文《二叉搜索树的本质》中我们提到,二叉搜索树在实践中有个很大的问题:树倾斜。按如下顺序插入数据会让二叉搜索树退化成普通链表,进而相关操作的时间复杂度退化到 O(n):
怎样让这棵树在写操作后仍然保持平衡呢?
R 教授一边呷着黑咖啡,一边盯着这棵“畸形”的二叉树发愣。
“要怎样才能在添加新节点的时候不会破坏树的平衡性呢——或许应该这样问:添加新节点的过程是如何破坏树的平衡性的?”R 教授紧锁双眉喃喃道。
忽然,他两眼放光,好像发现了什么!
“对!问题就出现在这:向下生长。”R 教授发现,每次添加新节点,都会给现有的叶节点生成新的孩子节点,如此,每次添加一个新节点,都使得树的高度加 1,最终变成上图中的链表。
“那么,如果不让这棵树向下生长呢?”这是个大胆的想法。
比如,添加元素 2 时,不是作为节点 1 的孩子节点,而是和 1 放在一起(按序)呢?像这样:
如此,树便不再生长了,继续:
树高度是不再生长了,但有个问题:整棵树只有一个节点了,不就退化成数组了吗?
既然它叫树,就必然需要生长。
“既然它要生长,又不能向下长,便只能向上长了。”
如何让树向上生长呢?
回想一下前文《二叉搜索树的本质》中我们如何通过二分法将有序数组构造成二叉树?
我们取有序数组的中间元素作为父节点 P,将左右子数组分别作为 P 的左右子树,通过这种方式我们最终构造出了一棵平衡二叉树。
这里我们不妨也借鉴这种思想试试。
当节点中元素数量达到 3 个时,我们取中间的元素作为父节点 P,两边的元素分别作为 P 的左右子节点。
如上图,原来包含 3 个元素的节点裂变成了 三个节点,以该节点为根的子树高度加 1。
为啥选择在包含 3 个元素的时候裂变,而不是 2 个或者更多呢?我们选择在奇数元素数的时候裂变,好进行左右对称操作。3 是自然数中除 1 以外最小的奇数,操作起来足够简单(至少理论上是)。
我们当然可以选择在节点包含 n 个元素的时候裂变。
也就是说,在我们设计的这种裂变型的搜索树中,一个节点最多可以包含 2 个元素,最少包含一个元素。当元素数超过 2 个时便会发生裂变,导致树(可能)向上生长——向上生长的含义是,裂变过程产生的父节点 P 总是向上冒泡,它可能在上层生成一个新节点(当原本上层没有节点时),也可能和上层的节点合并(当原本上层有一个单元素节点时),也可能导致上层节点继续裂变(当上层原本是 2 元素节点时)。
在二叉搜索树中,每个节点 P 最多可以有两个孩子节点:左孩子的值小于等于 P 的值,右孩子的值大于等于 P 的值。在我们新型搜索树中,一个节点最多可以有两个元素,当有两个元素 a, b 时,是可以表示三个区间的:小于 a 的元素集合 [,a)、介于 a,b 之间的元素集合 [a,b) 以及大于等于 b 的元素集合 [b,)。所以,当某节点 P 有两个元素时,它最多可以有三个子节点(想象成有序数组中两个元素分割开的三个区域)。
因而,在这种新型搜索树中,一个节点最多可能有 2 个或者 3 个子节点,我们“望文生义”地给它起个名字就叫 2-3 树。
我们按照上面的思想按序插入 1 ~ 11 的元素,看看最终生成的 2-3 树是怎样的。
在上图中,我们总是将新元素添加到已有的叶节点中,而不是直接创建新节点,所以添加元素本身并不会导致树高度发生改变(甚至不会导致节点数量发生改变)。只是在添加新元素后,可能导致既有节点分裂,进而导致节点数以及数高度发生变化。
进一步,我们发现,只有一种情况会导致树高度增加:根节点分裂——当根节点填入第三个元素时,会将中间的元素提升作为新根,而该元素左右两边的元素分别裂变成左右子树。该过程是对称的(左右子树高度同时加 1),因而无论如何裂变,整棵树都是平衡的。
也就是说,通过自下而上裂变式生长,真的能保证搜索树的平衡性(至少插入元素时是这样)——发现这点后,R 教授乐坏了,赶紧开瓶香槟以示庆祝。
R 教授一边呷着香槟一边试图实现这种神奇的搜索树。
不过在实现上他遇到了问题。
他发现这种看似很简单的数据结构,实现起来特复杂,需要分别考虑 2 节点和 3 节点的情况,写出来的代码又臭又长。
他在写删除逻辑的时候终于忍无可忍,将一大瓶香槟扔进了垃圾桶里。
“难道喝高了?不能啊,真理应该是简单的才对,这坨 SHIT 算个什么东西!”
于是他找来 L 教授帮忙。
“我觉得问题就出现在 2-3 上,它给人一种摇摆不定感。完美的设计应该是对称的。” L 教授故弄玄虚。
“但我没有什么好办法让它变成二叉树或者三叉树,一个节点在成为 3 节点之前必然要先成为 2 节点,所以这两种节点都必然会实际存在。”
“嗯,所以两类节点并存属于逻辑事实。” L 教授盯着面前的 2-3 树若有所思。
“不过我们能否在实现上消除一类节点呢——我的意思是,能否用 2 节点来表示 3 节点?”
“这是什么鬼?” R 教授疑惑地望着 L 教授。
L 教授解释道:“无论是 2 节点还是 3 节点,它们在逻辑上都是等价的:都是表示有序序列(有序数组),而且相互之间很容易转化。”
我们可以用如下三种代码来说明情况:
/**
* 最基本的数组表示法
*/
var arr = [...[-∞, a), a, ...[a, b), b, ...[b, ∞)]
/**
* 2-3 搜索树的 3 节点表示法
*/
var node = {
// 本节点元素数量
size: 2,
// 元素数组,升序排列
elements: [a, b],
// 子节点数组
// node1 子树中元素范围:[-∞, a)
// node2 子树中元素范围:[a, b)
// node3 子树中元素范围:[b, ∞)
children: [node1, node2, node3]
}
/**
* 二叉搜索树表示法
*/
var nodeB = {
element: b,
left: nodeA,
// nodeZ 子树的元素范围:[b, ∞)
right: nodeZ
}
var nodeA = {
element: a,
// nodeX 子树的元素范围:[-∞, a)
left: nodeX,
// nodeY 子树的元素范围:[a, b)
right: nodeY
}
L 教授画出如下草图:
“如此,便可以用 2 节点来表示 3 节点,只不过我们将 a、b 之间的连线用特殊颜色标记以示区分。也就是说,我们完全可以用二叉树来表示 2-3 树!” L 教授语调高亢,得意之神情溢于言表。
为了着重表示左边的 3 节点如何拆分成右边的 2 节点,L 教授将 a、b 之间的连线用红色笔画出。
“有点意思!” R 教授看到这里两眼放光,不知是因为喝高了,还是因为兴奋。
“所以待分裂的 4 节点可以用两条红线来表示。” R 教授顺着 L 教授的思路画了下图:
如上图,四节点可以用连续两条红线表示 a、b、c 之间的关系,不过由于最终不可能存在 4 节点,也就意味着不可能存在两条连续的红线,它最终会裂变成 3 个 2 节点。
“不过,这种转换有何用?我们不过是把 2-3 树又变成了二叉树而已。” R 教授兴奋过后两眼又泛起了疑惑。
“我们可以将 2-3 平衡二叉树的特性应用于等价二叉搜索树上,如此,理论上便能保持二叉树的平衡性——既然它是由 2-3 树转换来的。”
“不过在继续之前,我们先做个小小的优化。” L 教授继续道,“因为我们编码的时候是基于节点(而不是边)来编码的,所以我们可以将边的颜色转移给两端之一的节点——我们决定统一转给下端节点。”
如下图:
至此,这棵转换来的二叉搜索树中存在两种节点:红色节点和黑色节点。为了避免每次都叫“转换后的二叉树”,我们给它起个名字,不妨就叫红黑树吧。
“很形象的名字!”
我们前面已经得到了一个特性:由于 2-3 树中不可能存在 4 节点(拥有 3 个元素,4 个子节点的节点),对应地,红黑树中不可能存在两个连续的红节点。
“对,这是个显而易见却十分重要的特性。” R 教授接茬道,“另一个重要问题是:红黑树中如何表示 2-3 树的平衡特性,即所有的叶子节点到根节点的简单路径相等。”
L 教授盯着 2-3 树和转换后的红黑树思索良久,道:“2-3 树中所有叶子节点到根节点形成的简单路径上拥有的节点数量都相等。对于 2 节点,由于和红黑树中的(黑色)节点一一对应,不难处理;对于 3 节点,它在红黑树中其实由两个节点——一红一黑——构成,如果我们将这两个节点合二为一,那么红黑树中从任何叶节点到根节点的简单路径上节点数量也应该都是相同的。”
“也就是说,红黑树中,从任何叶节点到根节点的简单路径上,忽略掉所有红色节点后,剩下的节点——也就是黑色节点——数量是相同的。”
至此,我们得到了红黑树两条最重要的特性(核心特性):
特性 1: 不能存在两个连续的红节点;
特性 2: 从任何叶节点到根节点的简单路径上的黑节点数量是相同的;
因为 2-3 搜索树的任何子树也是 2-3 搜索树,所以以上两条性质对于红黑树的任何子树也都成立。
另外,显而易见,树根节点一定是黑色的——因为它的上游不可能有节点跟它组成 2-3 树中的 3 节点。
特性 3: 树根节点是黑色的。
L 教授继续道:“我想再加个人为的限制。前面我们将 2-3 树转成二叉树的时候发现,那条红线既可以往左倾,也可以往右倾。为了处理方便,我们限制只能往左倾,也就是让两个元素中大的那个作为父节点(黑色),小的作为左子节点(红色)。这样的红黑树不妨叫左倾红黑树。”
特性 4: 红节点必须是其父节点的左子节点。
最后,我们将一棵 2-3 树用红黑树完整表示出来:
“嗯,性质 1 和 2 确实满足,不过有个大问题:转成红黑树后,树的平衡性被破坏了!” R 教授盯着这棵红黑树大失所望。
“的确。3 节点转成二叉树中的两个 2 节点后,有可能增加树高度。因为二叉树本质上仍然是自上而下生长的(而非自下而上裂变的),所以注定了它不可能是一棵完美的平衡树。我们所追求的是实践意义上的近似平衡性。” L 教授略显尴尬地解释道。
“嗯,那就将真理交给实践去检验吧。现在我们不妨探索如何添加和删除元素。”
先看看插入元素 1.5。
在 2-3 树中的插入操作:
转成红黑树:
貌似很简单(先不管红黑树中为何 1 和 1.5 调换了位置)。
再看看插入元素 11。
首先找到相关叶节点 node(9,10),插入 11 得到 node(9,10,11),这是个 4 节点,会发生分裂,将 10 提升到上层构成 node(6,8,10),这仍然是个 4 节点继续发生分裂,最终波及到根节点。如图:
在这个例子中,无论 2-3 树还是红黑树的结构都发生了很大的变化,看来这“级联炸弹”威力不小。
“虽然我们总能够先操作 2-3 树,然后将结果映射到红黑树,但这在实现上并不可行——到目前为止我尚未发现将 2-3 树转换成红黑树其实现意义何在。” R 教授一本正经。
“的确,如果红黑树不能降低 2-3 树在实现上的复杂性,便没有半毛钱的意义。所以我们必须从二叉搜索树本身来解决元素操作问题。不过,先容我喝杯咖啡提提神。” L 教授说着走向咖啡机。
喝完咖啡,听了段莫扎特,两位教授继续研究红黑树。
插入或者删除节点后,必须对红黑树执行一系列操作来维护红黑树的特性。由于红黑树是二叉搜索树,所以操作后必须同时满足二叉搜索树的性质。
“我发现前面插入元素 11 后,虽然导致整棵红黑树结构变化很大,不过还是有规律的。我感觉好像将节点 4 围绕节点 8 逆时针旋转了一下,将 4 和 8 调换了父子关系,然后将 8 的左子节点给 4,整个二叉搜索树性质并未改变。” R 教授惊喜的说到。
“对!这真是个奇怪但有用的性质,我们不妨称它为左旋吧——因为被旋的节点在轴的左边。” L 教授补充道。
对应地就有右旋:
左右旋能保证二叉搜索树性质不变(请仔细分析上两图中字母大小关系,其中椭圆节点表示子树),如果在旋转中同时保证所涉及路径上的黑色节点数量不变,则旋转操作便可以用于维护红黑树性质。
另外还有一种显而易见的操作:颜色翻转。如图:
上图左侧,假设所有叶节点到根节点构成的简单路径上黑节点数量都相同,但 9 和 10 都是红节点,不符合特性 1。由于黑节点 8 的两个子节点都是红节点,我们将黑节点 8 和它两个子节点颜色交换,整棵树仍然满足红黑树性质。
接下来我们看看能否通过上面介绍的左旋、右旋和颜色翻转来维护红黑树的性质(包括二叉搜索树的性质)。
当我们向红黑树中插入一个新元素时,首先要决定该元素的初始颜色。
假设新节点初始化为黑色,那么它必然会导致某条路径的黑色节点数加 1,进而破坏红黑树性质。
如果初始化为红色,那么它本身不会导致黑节点增加(不违反特性 2),但可能会违反特性 1,即出现两个连续的红色节点(当其父节点也是红色时),但也有可能不违反任何特性。
所以我们决定让新节点初始化为红色,然后试图通过“三板斧”来修复特性 1。
场景一:待插入节点的父节点是黑色(有两种情况):
注意上图第二种情况,左旋后,由于 x 原本的黑色转移给了其父节点 y,自己变成了红色,所有经过 x 的简单路径(也必然经过 y)上的黑节点数并没有发生变化。
场景二:待插入节点的父节点是红色(也有两种情况,不过我们可以通过左旋操作让其变成一种情况):
我们将 x 的父节点 P 围绕 x 右旋看看情况:
如上图,右旋并适当调整节点颜色后,在保持二叉搜索树性质的前提下, α 子树和 β 子树中任何叶节点到根节点简单路径上的黑节点数量并没有发生变化,而且就图中几个节点而言,不再有两个连续的红节点了。
不过,我们得考察一下 α 子树和 β 子树。
由于原先 x 是红节点,所以 β 子树的树根必然是黑节点。然而 α 子树的树根既可能是黑节点也可能是红节点。
如果 α 子树的树根是红节点,则节点 P 的颜色违反了特性 1。
我们再观察节点 x、a、P 的颜色关系,发现可以翻转:
如图,翻转颜色后,不会改变 x 子树中任何一条路径上黑节点数量,而且解决了 P 和 α 子树可能的颜色冲突。
不过,细心的你肯定发现问题了。颜色翻转并没有解决问题,只是将可能的冲突从下面转移到了上面:翻转后,x 的颜色有可能跟其父节点颜色冲突(同为红色)。
另外,如果 x 是 x.parent 的右子节点,则违反了特性 3——不过这一点倒可以通过左旋解决。
因而,翻转颜色后,我们还要判断 x.parent 的颜色,如果是黑色则万事大吉,倘若是红色,则需要递归处理,直到整棵树的根节点。
透过现象看本质,场景二(连续两个红节点)情况下,我们的目的是让两个红节点向上合并成一个红节点以消除冲突。向上合并的过程有可能造成新的冲突(新的连续红节点),所以处理过程是递归的,有可能一直递归到整棵树的根节点。该向上合并的过程,类比到 2-3 树就是向上裂变的过程(因为连续两个红节点对应到 2-3 树上就是一个待裂变的 4 节点),裂变也是递归的,最终可能波及到根节点。
红节点合并与裂变的对应关系如下:
上图中一些特殊情况:
最后一个问题是,当红色合并操作递归到整棵树的根节点时会发生什么?
答案是:直接将根节点变成黑色,然后结束战斗。
我们看看递归到根节点时的几种情况:
无论是哪种情况,将根节点变成黑色后都仍然满足红黑树的性质:
从上面例子可知,当我们想将右倾红节点变成左倾红节点以满足特性 4 时要用到左旋,旋转后,父节点位置的颜色保持不变,原先右边的红色跑道左边去了。如图:
代码实现(TypeScript):
/**
* 将 h 围绕其右子节点左旋。
* @returns 返回新的父节点
*/
function rotateLeft(h: Node): Node {
assert(h && h.right)
const x = h.right
h.right = x.left
x.left = h
x.color = h.color
h.color = 'RED'
return x
}
对称地,右旋如下:
代码实现:
/**
* 将 h 围绕其左子节点右旋。
* @returns 返回新的父节点
*/
function rotateRight(h: Node): Node {
assert(h && h.left)
const x = h.left
h.left = x.right
x.right = h
x.color = h.color
h.color = 'RED'
return x
}
颜色翻转代码实现:
/**
* 将 h 和其两个子节点执行颜色翻转
* 要求 h 的两个子节点颜色相同而且和 h 的颜色不同
*/
function flipColors(h: Node) {
assert(h && h.left && h.right)
assert(h.left.color === h.right.color && h.left.color !== h.color)
const hColor = h.color
h.color = h.left.color
h.left.color = hColor
h.right.color = hColor
}
经分析发现,以上三种操作既不会破坏二叉搜索树的性质,也不会导致任何一条“叶-根”路径上黑节点数量发生改变,近乎完美——除了可能导致出现两个连续的红节点(破坏特性 1),因而实际使用中需要自下而上递归处理。
至此,我们完整实现一下红黑树的元素插入代码:
interface Value {
key: number;
val: unknown;
}
/**
* 定义树的节点
*/
interface Node extends Value {
left: Node | null;
right: Node | null;
color: 'RED' | 'BLACK';
}
class RedBlackTree {
protected root: Node | null
// 树节点数量
protected _size: number
public constructor() {
this.root = null
this._size = 0
}
/**
* 插入元素
* 从根节点往下找,直到找到合适的位置后插入
*
* @param key - 关键字
* @param val - 卫星数据
*/
public insert(key: number, val: unknown) {
this.root = this.innerInsert(this.root, { key, val })
// 处理完成后,将根节点变成黑色,结束战斗
this.root.color = 'BLACK'
this._size++
}
/**
* 递归地往 p 子树插入元素 value,并维护红黑树性质
* @param p
* @param value
* @returns 返回子树 p 新的根(插入并维护红黑性质后,新的根节点不一定还是原来的那个节点了)
*/
private innerInsert(p: Node | null, value: Value): Node {
if (p === null) {
// p 子树是个空指针(空树),创建并返回新节点
return this.newNode(value.key, value.val)
}
// p 是非空子树,则视情况将 value 插入到 p 的左子树或者右子树中
if (value.key < p.key) {
p.left = this.innerInsert(p.left, value)
} else {
p.right = this.innerInsert(p.right, value)
}
// 修复 p 子树的红黑性质
return this.balance(p)
}
/**
* 对以 p 为根的子树执行红黑平衡修复让该子树符合红黑树的性质
* 注意:情况一、二、三必须按顺序执行,因为前面情况的解决可能带来后面的情况
* @param p 子树的根
* @returns 修复完成后返回新的根(不一定是原先那个 p 了)
*/
private balance(p: Node): Node {
// 情况一(右倾),执行左旋
if (!this.isRed(p.left) && this.isRed(p.right)) {
p = this.rotateLeft(p)
}
// 情况二:连续两个左倾的红节点
if (this.isRed(p.left) && this.isRed(p.left.left)) {
p = this.rotateRight(p)
}
// 情况三:一个黑节点下面挂两个红节点,其中右边的红节点违反了“左倾”原则,通过翻转颜色解决
if (this.isRed(p.left) && this.isRed(p.right)) {
this.flipColors(p)
}
return p
}
/**
* 新节点默认是红色
*/
protected newNode(key: number, val: unknown): Node {
return { key, val, left: null, right: null, color: 'RED', size: 1 }
}
/**
* 让节点 h 相对于 h.right 执行左旋
*/
protected rotateLeft(h: Node): Node {
// 见前面实现
}
/**
* 让节点 h 相对于 h.left 执行右旋
*/
protected rotateRight(h: Node): Node {
// 见前面实现
}
/**
* 翻转颜色
*/
protected flipColors(h: Node) {
// 见前面实现
}
/**
* 判断节点 node 是否为红色
* 规定 null 节点是黑色
*/
protected isRed(node: Node | null): boolean {
if (!node) {
return false
}
return node.color === 'RED'
}
}
树形数据结构中,删除操作总是最复杂的,红黑树也不例外。
“初步看,删除得分两种情况:被删除的节点是叶子节点,或者是非叶子节点......”
“你等等,我好像发现了个重大问题!” R 教授突然打断了 L 教授的滔滔不绝。
“啥?”被这样硬生生地打断,L 教授显然有些不爽。
“你说叶子节点——这里有问题!” R 教授指着下面这棵红黑树:
现在删掉叶子节点 7,会怎样?
“嗯......不会怎样,仍然满足红黑树全部特性,换句话说,它仍然是一棵红黑树。” L 教授若有所思,他好像也意识到了什么。
“这正是问题所在,这棵树删掉节点 7 后仍然是一棵合法的红黑树,但它却无法转换成 2-3 树!” R 教授道。
所以我们必须保证任何一条路径不会因为删掉一个节点而消失。上图中如果路径(7-6-8-4)不因删除 7 而消失,那么删掉 7 以后这个路径便少了一个黑节点,从而不再满足红黑树的性质。
“或许我们可以引入哨兵——具体来说,我们让每个实节点都必须有两个子节点,如果没有,则用哨兵节点代替。为了不让哨兵的颜色影响实节点的颜色,哨兵节点必须是黑色(如实节点是红色,如果哨兵也是红色,则会违反特性 1)。” L 教授说道。
“这是个好主意。既然这些哨兵都出现在空节点的位置上,不妨就称这些哨兵为 nil 节点吧。” R 教授说着画出添加哨兵后的红黑树:
也就是说,特性 2 所提及的“叶节点”实际上是指哨兵节点(nil),而不是最下面的实节点。
2-3 树必须是完全平衡的搜索树,而上面这个(删掉节点 7 后)有问题的 2-3 树并不是一棵平衡树,不过这点从图中并不能明显地看出来,如果转成代码则较明显:
var node = { // 本节点元素数量 size: 2, // 元素数组,升序排列 elements: [6, 8], // 子节点数组 // node1 的值是 [5] // node2 是 null // node3 的值是 [9, 10] children: [node1, null, node3] }
如上表示节点 node(6,8),从 children 可见,第二个子节点是 null,而其它两个均有实际子节点,因而这里不再平衡。
所以,实际上在设计 2-3 树的时候就应该引入哨兵节点的概念了,如果 2-3 树引入了哨兵节点,那么转换为红黑树后,自然将哨兵的概念带过来了。
那么,我们如何删除上面的节点 7 呢?
我们知道,对于一条路径,删除路径上的红色节点不会影响路径上黑色节点数量。
所以,如果我们能先将节点 7 变成红色,然后再删除就行了。
如果节点 7 本身就是红色的,那么直接删除就行了。
如果节点 7 是黑色的(如上图所示),怎样将 7 变成红色节点呢?
我们只看节点 7 到根节点这条路径:
我们从这条路径的上游找一个红色节点(图中的 6)和节点 7 互换颜色,如此节点 7 变成了红色,且整条路径上黑色节点数量不变,然后再删除 7 即可。
不过,由于节点 6 变成了黑色,6 的左子树中所有路径的黑色节点数都多了一个,因而需要将节点 6 的左子结点也变成红色(即对节点 6 和其子节点执行 flipColors 操作)。
然而,如果整条路径上本来一个红节点都没有咋办呢?
可以将根节点变成红色——这相当于整棵树所有“叶-根”路径中黑节点数都减 1,红黑性质不变。然后在向下转移的过程中,根节点自然又变成黑色了。
如果要删除的节点不是叶子节点呢?比如如何删除节点 8?
在前文二叉搜索树中我们提过如何删除非叶子节点:将待删除节点和其后继节点(即比它大的最小节点)互换值,然后问题变为删除该后继节点——红黑树中后继节点是可以转换为叶子节点(这里指没有任何子节点的实节点)的。
此处只是描述了节点删除的大体逻辑思想,具体实现仍然是用前面提到的“三板斧”,而且实现代码要比插入逻辑复杂,此处不再给出完整代码实现,感兴趣可参见本人 github http://github.com/linzier/algo-ts 中红黑树的实现。
由于红黑树是二叉搜索树,所以可以直接用二叉搜索树的搜索逻辑实现。也就是说,节点的颜色并不影响搜索逻辑——这正是红黑树的一大优势。
至此,我们在两位教授的帮助下,大体厘清了红黑树的演化过程:
红黑树的完整实现(TypeScript 版本)见本人 github:http://github.com/linzier/algo-ts。
本文来自博客园,作者:林子er,转载请注明原文链接:http://www.cnblogs.com/linvanda/p/17400505.html
MySQL的最新版本8.0.29于2022年4月26日正式发行(GA)。MySQL8.0发布至今已经历4年(2018年4月19日GA),已经进入了标准生命周期的末期,如果您还在继续使用MySQL5.7版本,甚至是5.6版本,您现在应该认真考虑未来的数据库安全问题。MySQL8.0.29是一个维护版本,在这个版本里面做了大量的缺陷修复以及少数的改进,让我们快速浏览一下。缺陷修复MySQL8.0.29修复了160个缺陷与错误,特别感谢YuhuiWang和中国移动的BinWang,他们为MySQL贡献了两处修复代码。欢迎广大爱好者持续为MySQL提交错误报告和缺陷修复。功能改进MySQL8.0.29中做了少量的功能改进,包括未来版本中将使用的基础功能及将弃用的功能。用户需要注意如下内容: 字符串:服务器在使用“SHOW”语句输出、及报告无需字符时,使用utf8mb3代替之前使用的utf8。此外,服务器使用utf8mb3代替utf8用于填充数据字典表的字符集名称,将影响字符集和相关信息的显示。 时间格式:MySQL之前对时间格式的分隔符或空白等要求宽松,从此版本开始,推荐用户使用标准格式,使用
大家好,又见面了,我是全栈君。nginx多域名配置是在配置文件中建立多个server配置,在每个server配置中用server_name来对域名信息进行过滤。举个例子,下面是一个conf文件:server{listen80;server_namewww.web1.com;#绑定域名indexindex.htmindex.htmlindex.php;#默认文件root/home/www.web1.com;#网站根目录includelocation.conf;#调用其他规则,也可去除}server{listen80;server_namewww.web2.com;#绑定域名indexindex.htmindex.htmlindex.php;#默认文件root/home/www/web2.com;#网站根目录includelocation.conf;#调用其他规则,也可去除}复制以上配置信息就是在一个nginx配置中最简单的多域名配置方法,关于server_name,nginx官方还提供了很多正则匹配的过滤方式,详情请看nginx官方文档。注意事项特别要注意的是,在nginx的配置文件中只
上文:spring整合中间件(RocketMQ、kafka、RabbitMQ、ActiveMQ、ZeroMQ)ActiveMQZeroMQ是什么?ØMQ(也拼写作ZeroMQ,0MQ或ZMQ)是一个为可伸缩的分布式或并发应用程序设计的高性能异步消息库。它提供一个消息队列,但是与面向消息的中间件不同,ZeroMQ的运行不需要专门的消息代理(messagebroker)。该库设计成常见的套接字风格的API。--维基百科官网:https://zeromq.org/相关原理?建议参考这个文章:https://blog.csdn.net/weixin_37779156/article/details/102821706实现源码基于java项目结构│java_zeromq.iml │pom.xml │ └─src ├─main │├─java ││└─com ││└─hong ││Client.java ││Server.java ││ │└─resources └─test └─java复制源码实现spring_mq/java_zeromq/pom.xml<?xmlversion=&quo
快速上手如果不知道如何在Kotlin中写一个相当简单的Java表达式。这里有一个简单的诀窍,就是在AndroidStudio的Java文件中编写一段代码,然后将其粘贴到kt文件中,它会自动转换为Kotlin。Kotlin优势它更加易表现:这是它最重要的优点之一。你可以编写少得多的代码。它更加安全:Kotlin是空安全的,也就是说在我们编译时期就处理了各种null的情况,避免了执行时异常。你可以节约很多调试空指针异常的时间,解决掉null引发的bug。它可以扩展函数:这意味着,就算我们没有权限去访问这个类中的代码,我们也可以扩展这个类的更多的特性。它是函数式的:Kotlin是基于面向对象的语言。但是就如其他很多现代的语言那样,它使用了很多函数式编程的概念,比如,使用lambda表达式来更方便地解决问题。其中一个很棒的特性就是Collections的处理方式。我稍后会进行介绍。它是高度互操作性的:你可以继续使用所有用Java写的代码和库,甚至可以在一个项目中使用Kotlin和Java两种语言混合编程。一行Java一行Kotlin,别提有多风骚了。好了,话不多说了,来一看看本文的正文吧很多时
VBA是什么 自己的理解:VBA就是一种语言,你用符合语言规则的语句写出来后,VBA解析器能够完全认识的话,它就能按照规则进行执行。和我们日常的语言中文、英文等是一个道理。你只有和懂中文的说中文,对方才能知道你说的是什么,你对不懂的人说,那也等于白说。而且我认为它比我们日常用语简单太多了,学起来是不难的。(官方说明请baidu)如何开始 VBA不能单独使用,必须和某一种文档在一起。我们以Excel为例,首先为了以后方便使用,需要进行一些简单设置: 把菜单开发工具显示出来方便以后打开VBA编辑器(点“VisualBasic”打开的那个界面)、设置宏安全性是为了能够打开文件就执行程序(这一步设置后,一定要关闭所有的Excel)、VBA编辑里的设置我的已经设置好了,按图中勾选即可。 使用录制功能开始第1个程序 ExcelVBA有强大的录制功能,可以记录你的操作过程,把操作过程转变为代码,对于初学者这是个很好的学习工具: 把窗口设置如图,方便查看。 点击录制,会提示让我们设置一些东西,都按默认。开始录制后,VBA编辑器里就多了1个叫做“模块1”的东西,这个东西就是写代码的地方。 图
网络杂谈——聊聊NDS解析一、引言 在浏览器中输入一个地址,点击回车之后发生了什么?这是一个面试中常见的问题,这个看似常见简单的操作,其中却隐藏了大量复杂的互联网技术。本篇博客,我们就聊一聊网上冲浪的第一步:DNS解析。 DNS解析是一种服务,其又被称为域名解析。它的作用是将域名解析到具体的网络IP地址,以便进行后续的网络连接操作。DNS解析说起来也简单,从表面上看,就是通过一个查询服务,将域名映射成IP地址,可以往深处推敲,你就会发现其实并没有那么简单,世界上有无数的终端接入互联网,DNS服务是如何从浩如烟海的数据中找到目标数据的,DNS服务是如何保证搜索性能的等等都是值得讨论的话题。二、分层的网络 在深入了解DNS解析之前,首先需要对我们当下使用的网络系统有大致的了解。关于网络系统的分层,流行的有OSI的7层模型与TCP/IP的五层模型,其中关系如下图所示:以OSI的7层网络模型为例,其中每一层都负责对应的协议,两台终端在进行交互时,同层之间进行交互。我们从上到下来看:应用层:顾名思义应用层的主要作用是搭建应用,其负责实现应用层面的协议,例如文件传输FTP协议,还有我们最
版权声明:本文为博主原创文章,未经博主允许不得转载。https://blog.csdn.net/wzy0623/article/details/80269362本实验将应用OushuDB数据库,为一个销售订单系统建立数据仓库。通过这个简单的示例,讨论如何利用OushuDB提供的特性,在Hadoop上建立数据仓库系统。本篇说明示例的业务场景、数据仓库架构、实验环境、源和目标库的建立过程、测试数据和日期维度的生成。后面陆续进行初始ETL、定期ETL、调度ETL工作流自动执行、OLAP等实验。目的是演示以OushuDB代替传统数据仓库的具体实现过程。 一、业务场景1.操作型数据源 示例的操作型系统是一个销售订单系统,初始时只有产品、客户、销售订单三个表,实体关系图如图1所示。 图1 该场景中的表及其属性都很简单。产品表和客户表属于基本信息表,分别存储产品和客户的信息。产品只有产品编号、产品名称、产品分类三个属性,产品编号是主键,唯一标识一个产品。客户有六个属性,除客户编号和客户名称外,还包含省、市、街道、邮编四个客户所在地区属性。客户编号是主键,唯一标识一个客户。在实际应用中,基本信息表
相关RX文章请看: SNS项目笔记<七>--深入探究RXjs SNS项目笔记<四>--RXjs简要用法 1、封装的provider代码:import{Injectable}from'@angular/core'; import'rxjs/add/operator/map'; import{Subject}from"rxjs/Subject"; import{Observable}from'rxjs/Observable'; import{ToastController}from'ionic-angular'; /* GeneratedclassfortheRxbusprovider. Seehttps://angular.io/docs/ts/latest/guide/dependency-injection.html formoreinfoonprovidersandAngularDI. */ @Injectable() exportclassRxbus{
docker-compose.yml是Compose的默认模板文件。该文件有多种写法,例如Version1fileformat、Version2fileformat、Version2.1fileformat、Version3fileformat等。其中,Version1fileformat将逐步被被弃用;Version2.x及Version3.x基本兼容,是未来的趋势。考虑到目前业界的使用情况,本节只讨论Version2fileformat下的常用命令。(1)build配置构建时的选项,Compose会利用它自动构建镜像。build的值可以是一个路径,例如:build:./dir复制也可以是一个对象,用于指定Dockerfile和参数,例如:build: context:./dir dockerfile:Dockerfile-alternate args: buildno:1复制(2)command覆盖容器启动后默认执行的命令。示例:command:bundleexecthin-p3000复制也可以是一个list,类似于Dockerfile中的CMD指令,格式如下:command:[b
从今天开始重新学习(以前学的太匆忙)Hibernate,这篇文章主要就一下几点进行讲解和说明:Hibernate的基本介绍Hibernate的作用Hibernate基本配置Hibernate的基本介绍:Hibernate最开始的作者是GavinKing,是澳大利亚人,在工作中因为不满EJB的种种不足,而自行花费两年的时间开发出最原始的Hibernate,后来被Jboss收购了GavinKing所在的公司(最主要是看上了Hibernate),后来的Jboss被红帽收购,所以现在Hibernate为红帽旗下的产品。讲到了Hibernate怎么能不提一下什么叫做JPA(JavapersistenceAPI),是JavaEE5标准的ORM接口标准,是一种规范和接口,并不是ORM的具体实现,用于实现这一套规范的框架有很多,其中Hibernate就是一个这样的框架。JPA,ORM,Hibernate之间的关系: ORM是一种思想,JPA是这种思想的规范约束,Hibernate是这种思想和规范的具体实现。Hibernate的作用:说到了作用就自然而然的想到了Hibernate的工作大体工作模式:
显性URL转发/隐性URL转发其实URL转发里面的两种转发方式,根据跳转后的是否改变域名来判断显性还是隐形。当然根据不同的需要,可以选择不同的转发方式。今天小编为大家介绍的是隐/显性URL转发记录添加方式。显性URL转发/隐性URL转发URL是统一资源定位符,对可以从互联网上得到的资源的位置和访问方法的一种简洁的表示,是互联网上标准资源的地址。互联网上的每个文件都有一个唯一的URL,它包含的信息指出文件的位置以及浏览器应该怎么处理它。URL转发,是通过服务器的特殊设置,将访问您当前域名的用户引导到您指定的另一个网络地址。地址转向(也可称“URL转发”)即将一个域名指向到另外一个已存在的站点。域名指向可能这个站点原有的域名或网址是比较复杂难记的。隐性转发:用的是iframe框架技术,非重定向技术;如果跳转后,浏览器地址栏还是该域名,称为隐性URL转发。注:目标地址不允许被嵌套时,则不能使用隐性转发(如QQ空间,不能使用隐性转发)。显性转发:用的是301重定向技术;如果跳转后,浏览器地址栏变成另外一个域名,则称为显性url转发。隐/显性URL转发记录添加方式显性URL转发/隐性URL转发A
前言 在上一篇中我们介绍了Logstash快速入门,本文主要介绍的是ELK日志系统中的Logstash的实战使用。实战使用我打算从以下的几个场景来进行讲解。 时区问题解决方案 在我们使用logstash将采集的数据传输到ES中的时候,会发现采集的时间@timestamp的时间和我们本地的不一致,这个主要是因为时区的问题导致的,我们在计算时间的时候需要将这个时间增加8小时,但是这样会很不方便。为了永久解决这个问题,我们可以在logstash中的filter中对该字段进行转换,增加8小时。 添加的配置如下: ruby{ code=>"event.set('timestamp',event.get('@timestamp').time.localtime+8*60*60)" } ruby{ code=>"event.set('@timestamp',event.get('timestamp'))" } mutate{ remove_field=>["timestamp"] } 复制 原本示例图: 添加配置之后的示例图: 可以看到添加配置之后@timestamp时间已
笛卡尔树(CartesianTree),是一种二叉树,每个节点都有两个信息\((x_i,y_i)\),其中把\(x_i\)单独拎出来看是一棵二叉搜索树(\(ls_u<u<rs_u\)),而把\(y_i\)拎出来是一个小根堆(\(u<ls_u,rs_u\))。 废话不说,先看一道模板题P5854【模板】笛卡尔树,这道题题意就是说: 给你一个排列\((p_1,p_2,\cdots,p_n)\),构造信息为\((i,p_i)\)的笛卡尔树。 这道题可以\(p_1\)到\(p_n\)依次加进来(事实上所有建笛卡尔树都是先按\(x_i\)排序的,只不过这个\(i\)本来就是有序的)。 然后你可以想到每次肯定是尽量往右插,这样才能得到结果。一个事实是,笛卡尔树的构建是唯一的(笔者暂时并未想清楚原因,欢迎评论区留言)。 更具体地来说,每一次找到一个最大的比当前要插的数小的最大的点,设为\(v\),则将\(v\)的右儿子设为当前点(如果\(v\)存在的话)。如果\(v\)已经有右儿子了的话,就将当前的点接在\(v\)和\(rs_v\)中间。 代码如下(貌似要进行一些卡常?): /
目录1,前言2,安装3,配置4,运行 1,前言 项目写完了准备上线,发现打包后的包很大怎么办?,但是打包后的文件晦涩难懂,如何了解打包文件的细节,到底是哪里占了体积,了解某一个文件是由哪些内容打包而成,从而针对性的优化项目体积。下面给大家介绍一种工具。 webpack-bundle-analyzer 2,安装 #NPM npminstall--save-devwebpack-bundle-analyzer #Yarn yarnadd-Dwebpack-bundle-analyzer 复制 3,配置 webpack-bundle-analyzer自定义属性(更详细配置戳这里): 属性名 值 说明 analyzerMode 'server','static','json','disabled' Default:'server',分析报告的生成方式 analyzerHost String Default:127.0.0.1,分享报告本地服务器地址 analyzerPort Number Default:8888,分享报告本地端口地址 openAnalyzer
浏览器运行机制图: 浏览器的运行机制:layout:布局; 1、构建DOM树(parse):渲染引擎解析HTML文档,首先将标签转换成DOM树中的DOMnode(包括js生成的标签)生成内容树(ContentTree/DOMTree); 2、构建渲染树(construct):解析对应的CSS样式文件信息(包括js生成的样式和外部css文件),而这些文件信息以及HTML中可见的指令(如<b></b>),构建渲染树(RenderingTree/FrameTree); 3、布局渲染树(reflow/layout):从根节点递归调用,计算每一个元素的大小、位置等,给出每个节点所应该在屏幕上出现的精确坐标; 4、绘制渲染树(paint/repaint):遍历渲染树,使用UI后端层来绘制每个节点。 重绘(repaint或redraw):当盒子的位置、大小以及其他属性,例如颜色、字体大小等都确定下来之后,浏览器便把这些原色都按照各自的特性绘制一遍,将内容呈现在页面上。 重绘是指一个元素外观的改变所触发的浏览器行为,浏览器会根据元素的新属性重新绘制,使
本文地址 目录 目录目录Java及Kotlin中的可变参数varargJava中可变参数的规则可变参数的本质是数组Java中对应数组T[]Kotlin中可能对应Array或IntArray等衍生数组可变参数的应用可变参数的声明可变参数是否可为空可变参数的调用Kotlin类型转换 Java及Kotlin中的可变参数vararg Java中可变参数的规则 可变参数只能出现在参数列表的最后 用...代表可变参数,...位于变量类型和变量名之间 在方法体中以数组的形式访问可变参数 Kotlin中的规则类似 可变参数的本质是数组 Java中对应数组T[] publicstaticvoidhello(String[]array,String...args){ Class<?>stringArrayClass=Array.newInstance(String.class,0).getClass();//获取数组的Class System.out.println(array.getClass()==args.getClass());//true System.out.print
RabbitMQ—topic 一、topic交换器(主题交换器) 发送到topic交换器的消息不能具有随意的routing_key——它必须是单词列表,以点分隔。这些词可以是任何东西,但通常它们指定与消息相关的某些功能。一些有效的routing_key示例:“stock.usd.nyse”,“nyse.vmw”,“quick.orange.rabbit”。routing_key中可以包含任意多个单词,最多255个字节。 绑定键也必须采用相同的形式。topic交换器背后的逻辑类似于direct交换器——用特定路由键发送的消息将传递到所有匹配绑定键绑定的队列。但是,绑定键有两个重要的特殊情况:-*(星号)可以代替一个单词。-#(井号)可以替代零个或多个单词。 通过下面这个示例可以很容易看明白这一点: 在这个例子中,我们将发送一些都是描述动物的信息。将使用包含三个词(两个点)的路由密钥发送消息。路由键中的第一个单词将描述速度,第二个是颜色,第三个是种类:“<speed>.<colour>.<species>”。 我们创建了三个绑定关系:Q1与绑定键“*.
前言 首先声明,这又是一个小白从入门到进阶系列。 SignalR这个项目我关注了很长时间,中间好像还看到过微软即将放弃该项目的消息,然后我也就没有持续关注了,目前的我项目中使用的是自己搭建的WebSocket,连接管理和消息推送都是统一维护;前段时间编写了Asp.NETCore轻松学系列,现在腾出了一点时间,抱着学习的心态,想把自己学习SignalR的过程写出来,就当笔记吧,再做笔记的过程中再加入实际的项目需求,一步一步的深入学习SignalR,正所谓技多不压身吧。有想要一起学习的同学,可以关注我,大家一起学习,一起进步。 SignalR简单介绍 根据官方文档介绍,SignalR是一个面向开发人员的库,其本质是对Web实时连接(WebSocket)的抽象和封装,使用SIgnalR,可以避免自己编写和管理Web实时连接,并获得更多客户端的兼容性,截止本文发文为止,SignalRnpm包的版本是@aspnet/signalr-1.1.2,在Asp.NETCore中,SignalR不支持自动重连,如果客户端连接断开,必须显示重连。话不多说,下面就开始干吧。 1.项目搭建 1.1搭建Asp.N
简单的说其实要理解C文件与头文件(即.h)有什么不同之处,首先需要弄明白编译器的工作过程,一般说来编译器会做以下几个过程: 1.预处理阶段 2.词法与语法分析阶段 3.编译阶段,首先编译成纯汇编语句,再将之汇编成跟CPU相关的二进制码,生成各个目标文件(.obj文件) 4.连接阶段,将各个目标文件中的各段代码进行绝对地址定位,生成跟特定平台相关的可执行文件,当然,最后还可以用objcopy生成纯二进制码,也就是去掉了文件格式信息。(生成.exe文件) 编译器在编译时是以C文件为单位进行的,也就是说如果你的项目中一个C文件都没有,那么你的项目将无法编译,连接器是以目标文件为单位,它将一个或多个目标文件进行函数与变量的重定位,生成最终的可执行文件,在PC上的程序开发,一般都有一个main函数,这是各个编译器的约定,当然,你如果自己写连接器脚本的话,可以不用main函数作为程序入口!!!! (main.c文件目标文件可执行文件) 有了这些基础知识,再言归正传,为了生成一个最终的可执行文件,就需要一些目标文件
0、引言 写作目的:只是为了学习一下DNN的用法 基本思路: 首先,将学生成绩(平时成绩x、期末成绩y:csv格式)装载; 接着,将成绩数据标准化。(PS:虽然这里的成绩已经[0~100]之间了,本文是为了学习DNN,故不省略这一步) 接着,将平时成绩x,期末成绩y进一步拆分(按比例,如20%)为训练数据和测试数据。PS:测试数据用来检验训练出的模型性能 接着,创建DNN模型。依次设置一系列的参数。 接着,训练模型(fit函数)。model1.fit(x_train,y_train) 接着,评估模型性能。用测试集数据评估,还可以用训练集数据。如果性能不好,则调参(第四步) 最后,可以应用所得模型,进行期末成绩的预测。 实验环境 MacOSX python2 应用到的包(好多,缺什么你安装什么好了,有(很多)时候还会出错,自己百度、谷歌等可以解决,我花费了1天时间才搞好) 1、装载数据 预先加载包importcsv,os 装载数据,到score_data;标题写到score_header,代码如下: score_file='score.csv' ifnotos.pa
最近跟物联网行业和移动互联网行业的一些资深从业人员做了深入交流,就物联网操作系统的概念和必要性、定位等进行了充分深入的沟通。首先说明的是,物联网操作系统的概念被广泛认同。同时,对物联网操作系统在整个物联网领域的功能和地位,又有了更进一步的认识。下面简单总结,供业界的朋友们参考评论。 物联网操作系统的最基本功能,与Android操作系统在移动互联网领域的地位和作用类似。先看一下Android,其最大的贡献在于,实现了智能终端硬件和软件的分离。任何应用程序开发者,基本不用考虑智能终端的物理硬件配置(CPU型号、内存、各种外设等),只需根据Android提供的编程接口编写应用程序,就可以运行在所有基于Android的智能终端上。硬件的功能是有限的,如果软件和硬件紧密捆绑不分离,则整个系统的功能也是有限的。但是一旦把硬件功能剥离出来,则通过软件的变动,可以使得整个系统的功能大大扩充。举例来说,带闪光灯的拍照手机,如果硬件和软件捆绑,则其功能就仅仅局限于一台照相机和一部手机。但是软硬件分离后,就可以变成手电筒、信号灯等原来无法实现的功能。对于物联网来说,要实现类似移动互联网一样的良性发展,也需要