二叉查找树

二叉查找树,又叫二叉排序树,二叉搜索树,是一种有特定规则的二叉树,定义如下:

  1. 它是一颗二叉树,或者是空树。
  2. 左子树所有节点的值都小于它的根节点,右子树所有节点的值都大于它的根节点。
  3. 左右子树也是一颗二叉查找树。

二叉查找树的特点是,一直往左儿子往下找左儿子,可以找到最小的元素,一直往右儿子找右儿子,可以找到最大的元素。

看起来,我们可以用它来实现元素排序,可是我们却使用了二叉堆来实现了堆排序,因为二叉查找树不保证是一个平衡的二叉树,最坏情况下二叉查找树会退化成一个链表,也就是所有节点都没有左子树或者没有右子树,树的层次太深导致排序性能太差。

使用二分查找,可以很快在一颗二叉查找树中找到我们需要的值。

我们来分析二叉查找树添加,删除,查找元素的方法。

一、添加元素

以下是一个二叉查找树的表示:

// 二叉查找树
type BinarySearchTree struct {
Root *BinarySearchTreeNode // 树根节点
} // 二叉查找树节点
type BinarySearchTreeNode struct {
Value int64 // 值
Times int64 // 值出现的次数
Left *BinarySearchTreeNode // 左子树
Right *BinarySearchTreeNode // 右字树
} // 初始化一个二叉查找树
func NewBinarySearchTree() *BinarySearchTree {
return new(BinarySearchTree)
}

一个节点代表一个元素,节点的Value值是用来进行二叉查找的关键,当Value值重复时,我们将值出现的次数Times加 1。添加元素代码如下:

// 添加元素
func (tree *BinarySearchTree) Add(value int64) {
// 如果没有树根,证明是颗空树,添加树根后返回
if tree.Root == nil {
tree.Root = &BinarySearchTreeNode{Value: value}
return
} // 将值添加进去
tree.Root.Add(value)
} func (node *BinarySearchTreeNode) Add(value int64) {
if value < node.Value {
// 如果插入的值比节点的值小,那么要插入到该节点的左子树中
// 如果左子树为空,直接添加
if node.Left == nil {
node.Left = &BinarySearchTreeNode{Value: value}
} else {
// 否则递归
node.Left.Add(value)
}
} else if value > node.Value {
// 如果插入的值比节点的值大,那么要插入到该节点的右子树中
// 如果右子树为空,直接添加
if node.Right == nil {
node.Right = &BinarySearchTreeNode{Value: value}
} else {
// 否则递归
node.Right.Add(value)
}
} else {
// 值相同,不需要添加,值出现的次数加1即可
node.Times = node.Times + 1
}
}

如果添加元素时是颗空树,那么初始化根节点。

然后添加的值和根节点比较,判断是要插入到根节点左子树还是右子树,还是不用插入。

当值比根节点小时,元素要插入到根节点的左子树中,当值比根节点大时,元素要插入到根节点的右子树中,相等时不插入,只更新次数。

然后再分别对根节点的左子树和右子树进行递归操作即可。

二、查找最大值或最小值的元素

查找最大值和最小值比较简单,一直往左儿子往下找左儿子,可以找到最小的元素,一直往右儿子找右儿子,可以找到最大的元素。

// 找出最小值的节点
func (tree *BinarySearchTree) FindMinValue() *BinarySearchTreeNode {
if tree.Root == nil {
// 如果是空树,返回空
return nil
} return tree.Root.FindMinValue()
} func (node *BinarySearchTreeNode) FindMinValue() *BinarySearchTreeNode {
// 左子树为空,表面已经是最左的节点了,该值就是最小值
if node.Left == nil {
return node
} // 一直左子树递归
return node.Left.FindMinValue()
} // 找出最大值的节点
func (tree *BinarySearchTree) FindMaxValue() *BinarySearchTreeNode {
if tree.Root == nil {
// 如果是空树,返回空
return nil
} return tree.Root.FindMaxValue()
} func (node *BinarySearchTreeNode) FindMaxValue() *BinarySearchTreeNode {
// 右子树为空,表面已经是最右的节点了,该值就是最大值
if node.Right == nil {
return node
} // 一直右子树递归
return node.Right.FindMaxValue()
}

三、查找指定元素

二分查找的技巧也在这里有用武之地了:

// 查找节点
func (tree *BinarySearchTree) Find(value int64) *BinarySearchTreeNode {
if tree.Root == nil {
// 如果是空树,返回空
return nil
} return tree.Root.Find(value)
} func (node *BinarySearchTreeNode) Find(value int64) *BinarySearchTreeNode {
if value == node.Value {
// 如果该节点刚刚等于该值,那么返回该节点
return node
} else if value < node.Value {
// 如果查找的值小于节点值,从节点的左子树开始找
if node.Left == nil {
// 左子树为空,表示找不到该值了,返回nil
return nil
}
return node.Left.Find(value)
} else {
// 如果查找的值大于节点值,从节点的右子树开始找
if node.Right == nil {
// 右子树为空,表示找不到该值了,返回nil
return nil
}
return node.Right.Find(value)
}
}

如果是空树,返回 nil,否则与根节点比较。

如果刚刚好等于根节点的值,返回该节点,否则根据值的比较,继续往左子树或右字树递归查找。

四、查找指定元素的父亲

与查找指定元素一样,只不过追踪的是该元素的父亲节点。

// 查找指定节点的父亲
func (tree *BinarySearchTree) FindParent(value int64) *BinarySearchTreeNode {
if tree.Root == nil {
// 如果是空树,返回空
return nil
} // 如果根节点等于该值,根节点其没有父节点,返回nil
if tree.Root.Value == value {
return nil
}
return tree.Root.FindParent(value)
} func (node *BinarySearchTreeNode) FindParent(value int64) *BinarySearchTreeNode {
// 外层没有值相等的判定,因为在内层已经判定完毕后返回父亲节点。 if value < node.Value {
// 如果查找的值小于节点值,从节点的左子树开始找
leftTree := node.Left
if leftTree == nil {
// 左子树为空,表示找不到该值了,返回nil
return nil
} // 左子树的根节点的值刚好等于该值,那么父亲就是现在的node,返回
if leftTree.Value == value {
return node
} else {
return leftTree.FindParent(value)
}
} else {
// 如果查找的值大于节点值,从节点的右子树开始找
rightTree := node.Right
if rightTree == nil {
// 右子树为空,表示找不到该值了,返回nil
return nil
} // 右子树的根节点的值刚好等于该值,那么父亲就是现在的node,返回
if rightTree.Value == value {
return node
} else {
return rightTree.FindParent(value)
}
}
}

代码相应的进行了调整,方便获取到父亲节点。

如果返回的父亲节点为空,表示没有父亲。

五、删除元素

删除元素有四种情况:

  1. 第一种情况,删除的是根节点,且根节点没有儿子,直接删除即可。
  2. 第二种情况,删除的节点有父亲节点,但没有子树,也就是删除的是叶子节点,直接删除即可。
  3. 第三种情况,删除的节点下有两个子树,因为右子树的值都比左子树大,那么用右子树中的最小元素来替换删除的节点,这时二叉查找树的性质又满足了。右子树的最小元素,只要一直往右子树的左边一直找一直找就可以找到。
  4. 第四种情况,删除的节点只有一个子树,那么该子树直接替换被删除的节点即可。

代码实现如下:

// 删除指定的元素
func (tree *BinarySearchTree) Delete(value int64) {
if tree.Root == nil {
// 如果是空树,直接返回
return
} // 查找该值是否存在
node := tree.Root.Find(value)
if node == nil {
// 不存在该值,直接返回
return
} // 查找该值的父亲节点
parent := tree.Root.FindParent(value) // 第一种情况,删除的是根节点,且根节点没有儿子
if parent == nil && node.Left == nil && node.Right == nil {
// 置空后直接返回
tree.Root = nil
return
} else if node.Left == nil && node.Right == nil {
// 第二种情况,删除的节点有父亲节点,但没有子树 // 如果删除的是节点是父亲的左儿子,直接将该值删除即可
if parent.Left != nil && value == parent.Left.Value {
parent.Left = nil
} else {
// 删除的原来是父亲的右儿子,直接将该值删除即可
parent.Right = nil
}
return
} else if node.Left != nil && node.Right != nil {
// 第三种情况,删除的节点下有两个子树,因为右子树的值都比左子树大,那么用右子树中的最小元素来替换删除的节点。
// 右子树的最小元素,只要一直往右子树的左边一直找一直找就可以找到,替换后二叉查找树的性质又满足了。 // 找右子树中最小的值,一直往右子树的左边找
minNode := node.Right
for minNode.Left != nil {
minNode = minNode.Left
}
// 把最小的节点删掉
tree.Delete(minNode.Value) // 最小值的节点替换被删除节点
node.Value = minNode.Value
node.Times = minNode.Times
} else {
// 第四种情况,只有一个子树,那么该子树直接替换被删除的节点即可 // 父亲为空,表示删除的是根节点,替换树根
if parent == nil {
if node.Left != nil {
tree.Root = node.Left
} else {
tree.Root = node.Right
}
return
}
// 左子树不为空
if node.Left != nil {
// 如果删除的是节点是父亲的左儿子,让删除的节点的左子树接班
if parent.Left != nil && value == parent.Left.Value {
parent.Left = node.Left
} else {
parent.Right = node.Left
}
} else {
// 如果删除的是节点是父亲的左儿子,让删除的节点的右子树接班
if parent.Left != nil && value == parent.Left.Value {
parent.Left = node.Right
} else {
parent.Right = node.Right
}
}
}
}

首先查找到要删除元素的节点:tree.Root.Find(value),然后找到该节点父亲:tree.Root.FindParent(value),根据四种不同情况对删除节点进行补位。核心在于,第三种情况下,删除的节点有两个子树情况下,需要用右子树中最小的节点来替换被删除节点。

上面的代码可以优化,可以在查找删除元素的节点时顺道查出其父亲节点,不必要分开查询父亲节点,在第三种情况下查出右子树的最小节点时可以直接将其移除,不必递归使用tree.Delete(minNode.Value)

由于这种通用形式的二叉查找树实现甚少使用,大部分程序都使用了AVL树或红黑树,以上优化理解即可。

六、中序遍历(实现排序)

使用二叉查找树可以实现排序,只需要对树进行中序遍历即可。

我们先打印出左子树,然后打印根节点的值,再打印右子树,这是一个递归的过程。

// 中序遍历
func (tree *BinarySearchTree) MidOrder() {
tree.Root.MidOrder()
} func (node *BinarySearchTreeNode) MidOrder() {
if node == nil {
return
} // 先打印左子树
node.Left.MidOrder() // 按照次数打印根节点
for i := 0; i <= int(node.Times); i++ {
fmt.Println(node.Value)
} // 打印右子树
node.Right.MidOrder()
}

七、完整代码

package main

import (
"fmt"
) // 二叉查找树节点
type BinarySearchTree struct {
Root *BinarySearchTreeNode // 树根节点
} // 二叉查找树节点
type BinarySearchTreeNode struct {
Value int64 // 值
Times int64 // 值出现的次数
Left *BinarySearchTreeNode // 左子树
Right *BinarySearchTreeNode // 右字树
} // 初始化一个二叉查找树
func NewBinarySearchTree() *BinarySearchTree {
return new(BinarySearchTree)
} // 添加元素
func (tree *BinarySearchTree) Add(value int64) {
// 如果没有树根,证明是颗空树,添加树根后返回
if tree.Root == nil {
tree.Root = &BinarySearchTreeNode{Value: value}
return
} // 将值添加进去
tree.Root.Add(value)
} func (node *BinarySearchTreeNode) Add(value int64) {
if value < node.Value {
// 如果插入的值比节点的值小,那么要插入到该节点的左子树中
// 如果左子树为空,直接添加
if node.Left == nil {
node.Left = &BinarySearchTreeNode{Value: value}
} else {
// 否则递归
node.Left.Add(value)
}
} else if value > node.Value {
// 如果插入的值比节点的值大,那么要插入到该节点的右子树中
// 如果右子树为空,直接添加
if node.Right == nil {
node.Right = &BinarySearchTreeNode{Value: value}
} else {
// 否则递归
node.Right.Add(value)
}
} else {
// 值相同,不需要添加,值出现的次数加1即可
node.Times = node.Times + 1
}
} // 找出最小值的节点
func (tree *BinarySearchTree) FindMinValue() *BinarySearchTreeNode {
if tree.Root == nil {
// 如果是空树,返回空
return nil
} return tree.Root.FindMinValue()
} func (node *BinarySearchTreeNode) FindMinValue() *BinarySearchTreeNode {
// 左子树为空,表面已经是最左的节点了,该值就是最小值
if node.Left == nil {
return node
} // 一直左子树递归
return node.Left.FindMinValue()
} // 找出最大值的节点
func (tree *BinarySearchTree) FindMaxValue() *BinarySearchTreeNode {
if tree.Root == nil {
// 如果是空树,返回空
return nil
} return tree.Root.FindMaxValue()
} func (node *BinarySearchTreeNode) FindMaxValue() *BinarySearchTreeNode {
// 右子树为空,表面已经是最右的节点了,该值就是最大值
if node.Right == nil {
return node
} // 一直右子树递归
return node.Right.FindMaxValue()
} // 查找指定节点
func (tree *BinarySearchTree) Find(value int64) *BinarySearchTreeNode {
if tree.Root == nil {
// 如果是空树,返回空
return nil
} return tree.Root.Find(value)
} func (node *BinarySearchTreeNode) Find(value int64) *BinarySearchTreeNode {
if value == node.Value {
// 如果该节点刚刚等于该值,那么返回该节点
return node
} else if value < node.Value {
// 如果查找的值小于节点值,从节点的左子树开始找
if node.Left == nil {
// 左子树为空,表示找不到该值了,返回nil
return nil
}
return node.Left.Find(value)
} else {
// 如果查找的值大于节点值,从节点的右子树开始找
if node.Right == nil {
// 右子树为空,表示找不到该值了,返回nil
return nil
}
return node.Right.Find(value)
}
} // 查找指定节点的父亲
func (tree *BinarySearchTree) FindParent(value int64) *BinarySearchTreeNode {
if tree.Root == nil {
// 如果是空树,返回空
return nil
} // 如果根节点等于该值,根节点其没有父节点,返回nil
if tree.Root.Value == value {
return nil
}
return tree.Root.FindParent(value)
} func (node *BinarySearchTreeNode) FindParent(value int64) *BinarySearchTreeNode {
// 外层没有值相等的判定,因为在内层已经判定完毕后返回父亲节点。 if value < node.Value {
// 如果查找的值小于节点值,从节点的左子树开始找
leftTree := node.Left
if leftTree == nil {
// 左子树为空,表示找不到该值了,返回nil
return nil
} // 左子树的根节点的值刚好等于该值,那么父亲就是现在的node,返回
if leftTree.Value == value {
return node
} else {
return leftTree.FindParent(value)
}
} else {
// 如果查找的值大于节点值,从节点的右子树开始找
rightTree := node.Right
if rightTree == nil {
// 右子树为空,表示找不到该值了,返回nil
return nil
} // 右子树的根节点的值刚好等于该值,那么父亲就是现在的node,返回
if rightTree.Value == value {
return node
} else {
return rightTree.FindParent(value)
}
}
} // 删除指定的元素
func (tree *BinarySearchTree) Delete(value int64) {
if tree.Root == nil {
// 如果是空树,直接返回
return
} // 查找该值是否存在
node := tree.Root.Find(value)
if node == nil {
// 不存在该值,直接返回
return
} // 查找该值的父亲节点
parent := tree.Root.FindParent(value) // 第一种情况,删除的是根节点,且根节点没有儿子
if parent == nil && node.Left == nil && node.Right == nil {
// 置空后直接返回
tree.Root = nil
return
} else if node.Left == nil && node.Right == nil {
// 第二种情况,删除的节点有父亲节点,但没有子树 // 如果删除的是节点是父亲的左儿子,直接将该值删除即可
if parent.Left != nil && value == parent.Left.Value {
parent.Left = nil
} else {
// 删除的原来是父亲的右儿子,直接将该值删除即可
parent.Right = nil
}
return
} else if node.Left != nil && node.Right != nil {
// 第三种情况,删除的节点下有两个子树,因为右子树的值都比左子树大,那么用右子树中的最小元素来替换删除的节点,这时二叉查找树的性质又满足了。 // 找右子树中最小的值,一直往右子树的左边找
minNode := node.Right
for minNode.Left != nil {
minNode = minNode.Left
}
// 把最小的节点删掉
tree.Delete(minNode.Value) // 最小值的节点替换被删除节点
node.Value = minNode.Value
node.Times = minNode.Times
} else {
// 第四种情况,只有一个子树,那么该子树直接替换被删除的节点即可 // 父亲为空,表示删除的是根节点,替换树根
if parent == nil {
if node.Left != nil {
tree.Root = node.Left
} else {
tree.Root = node.Right
}
return
}
// 左子树不为空
if node.Left != nil {
// 如果删除的是节点是父亲的左儿子,让删除的节点的左子树接班
if parent.Left != nil && value == parent.Left.Value {
parent.Left = node.Left
} else {
parent.Right = node.Left
}
} else {
// 如果删除的是节点是父亲的左儿子,让删除的节点的右子树接班
if parent.Left != nil && value == parent.Left.Value {
parent.Left = node.Right
} else {
parent.Right = node.Right
}
}
}
} // 中序遍历
func (tree *BinarySearchTree) MidOrder() {
tree.Root.MidOrder()
} func (node *BinarySearchTreeNode) MidOrder() {
if node == nil {
return
} // 先打印左子树
node.Left.MidOrder() // 按照次数打印根节点
for i := 0; i <= int(node.Times); i++ {
fmt.Println(node.Value)
} // 打印右子树
node.Right.MidOrder()
} func main() {
values := []int64{3, 6, 8, 20, 9, 2, 6, 8, 9, 3, 5, 40, 7, 9, 13, 6, 8} // 初始化二叉查找树并添加元素
tree := NewBinarySearchTree()
for _, v := range values {
tree.Add(v)
} // 找到最大值或最小值的节点
fmt.Println("find min value:", tree.FindMinValue())
fmt.Println("find max value:", tree.FindMaxValue()) // 查找不存在的99
node := tree.Find(99)
if node != nil {
fmt.Println("find it 99!")
} else {
fmt.Println("not find it 99!")
} // 查找存在的9
node = tree.Find(9)
if node != nil {
fmt.Println("find it 9!")
} else {
fmt.Println("not find it 9!")
} // 删除存在的9后,再查找9
tree.Delete(9)
node = tree.Find(9)
if node != nil {
fmt.Println("find it 9!")
} else {
fmt.Println("not find it 9!")
} // 中序遍历,实现排序
tree.MidOrder()
}

运行程序后,结果:

find min value: &{2 0 <nil> <nil>}
find max value: &{40 0 <nil> <nil>}
not find it 99!
find it 9!
not find it 9!
2
3
3
5
6
6
6
7
8
8
8
13
20
40

八、总结

二叉查找树可能退化为链表,也可能是一颗非常平衡的二叉树,查找,添加,删除元素的时间复杂度取决于树的高度h

  1. 当二叉树是满的时,树的高度是最小的,此时树节点数量n和高度h的关系为:h = log(n)
  2. 当二叉树是一个链表时,此时树节点数量n和高度h的关系为:h = n

二叉查找树的效率来源其二分查找的特征,时间复杂度在于二叉树的高度,因此查找,添加和删除的时间复杂度范围为log(n)~n

为了提高二叉查找树查找的速度,树的高度要尽可能的小。AVL树和红黑树都是相对平衡的二叉查找树,因为特殊的旋转平衡操作,树的高度被大大压低。它们查找效率较高,添加,删除,查找操作的平均时间复杂度都为log(n),经常在各种程序中被使用。

二叉查找树是后面要学习的高级数据结构AVL树,红黑树的基础。

系列文章入口

我是陈星星,欢迎阅读我亲自写的 数据结构和算法(Golang实现),文章首发于 阅读更友好的GitBook

数据结构和算法(Golang实现)(27)查找算法-二叉查找树的更多相关文章

  1. 数据结构和算法(Golang实现)(26)查找算法-哈希表

    哈希表:散列查找 一.线性查找 我们要通过一个键key来查找相应的值value.有一种最简单的方式,就是将键值对存放在链表里,然后遍历链表来查找是否存在key,存在则更新键对应的值,不存在则将键值对链 ...

  2. 数据结构和算法(Golang实现)(28)查找算法-AVL树

    AVL树 二叉查找树的树高度影响了查找的效率,需要尽量减小树的高度,AVL树正是这样的树. 一.AVL树介绍 AVL树是一棵严格自平衡的二叉查找树,1962年,发明者Adelson-Velsky和La ...

  3. 数据结构和算法(Golang实现)(29)查找算法-2-3树和左倾红黑树

    某些教程不区分普通红黑树和左倾红黑树的区别,直接将左倾红黑树拿来教学,并且称其为红黑树,因为左倾红黑树与普通的红黑树相比,实现起来较为简单,容易教学.在这里,我们区分开左倾红黑树和普通红黑树. 红黑树 ...

  4. 数据结构和算法(Golang实现)(30)查找算法-2-3-4树和普通红黑树

    文章首发于 阅读更友好的GitBook. 2-3-4树和普通红黑树 某些教程不区分普通红黑树和左倾红黑树的区别,直接将左倾红黑树拿来教学,并且称其为红黑树,因为左倾红黑树与普通的红黑树相比,实现起来较 ...

  5. 数据结构和算法(Golang实现)(25)排序算法-快速排序

    快速排序 快速排序是一种分治策略的排序算法,是由英国计算机科学家Tony Hoare发明的, 该算法被发布在1961年的Communications of the ACM 国际计算机学会月刊. 注:A ...

  6. 数据结构和算法(Golang实现)(18)排序算法-前言

    排序算法 人类的发展中,我们学会了计数,比如知道小明今天打猎的兔子的数量是多少.另外一方面,我们也需要判断,今天哪个人打猎打得多,我们需要比较. 所以,排序这个很自然的需求就出来了.比如小明打了5只兔 ...

  7. 数据结构和算法(Golang实现)(19)排序算法-冒泡排序

    冒泡排序 冒泡排序是大多数人学的第一种排序算法,在面试中,也是问的最多的一种,有时候还要求手写排序代码,因为比较简单. 冒泡排序属于交换类的排序算法. 一.算法介绍 现在有一堆乱序的数,比如:5 9 ...

  8. 数据结构和算法(Golang实现)(20)排序算法-选择排序

    选择排序 选择排序,一般我们指的是简单选择排序,也可以叫直接选择排序,它不像冒泡排序一样相邻地交换元素,而是通过选择最小的元素,每轮迭代只需交换一次.虽然交换次数比冒泡少很多,但效率和冒泡排序一样的糟 ...

  9. 数据结构和算法(Golang实现)(21)排序算法-插入排序

    插入排序 插入排序,一般我们指的是简单插入排序,也可以叫直接插入排序.就是说,每次把一个数插到已经排好序的数列里面形成新的排好序的数列,以此反复. 插入排序属于插入类排序算法. 除了我以外,有些人打扑 ...

随机推荐

  1. 源码分析 Alibaba sentinel 滑动窗口实现原理(文末附原理图)

    要实现限流.熔断等功能,首先要解决的问题是如何实时采集服务(资源)调用信息.例如将某一个接口设置的限流阔值 1W/tps,那首先如何判断当前的 TPS 是多少?Alibaba Sentinel 采用滑 ...

  2. 如何用 Blazor 实现 Ant Design 组件库

    本文主要分享我创建 Ant Design of Blazor 项目的心路历程,已经文末有一个 Blazor 线上分享预告. Blazor WebAssembly 来了! Blazor 这个新推出的前端 ...

  3. Fiddler3 使用技巧

    1.Fiddler抓不到包怎么解决 (1)先确定是HTTP包抓不到还是HTTPS包抓不到.如果只是HTTPS包抓不到,说明是证书的问题,需要重新安装证书. (2)检查浏览器的HTTP代理设置是否正确, ...

  4. 常用正则表达式(手机号、邮箱、URL地址、身份证等等)

    一.前言 不好的习惯:1.每一次用到正则都是上网copy一份,也没有去学习思考,看看都是什么意思: 2.一个项目里不同的地方用到了相同的校验,一直在重复的copy代码,并没有统一起来,万一哪天要修改规 ...

  5. .NET Core使用NPOI导出复杂,美观的Excel详解

    前言: 这段时间一直专注于数据报表的开发,当然涉及到相关报表的开发数据导出肯定是一个不可避免的问题啦.客户要求要导出优雅,美观的Excel文档格式的来展示数据,当时的第一想法就是使用NPOI开源库来做 ...

  6. MySQL 教程--检视阅读

    MySQL 教程--检视阅读 准备:Windows 上安装 MySQL 教程地址,PHP语言基础 教程地址2 教程地址3,有讲数据库的备份和恢复 教程地址4,w3c.china,php基础,扩展阅读 ...

  7. 参加Folding@Home(FAH)项目,为战胜新冠肺炎贡献出自己的一份力量

    鉴于新冠病毒(COVID-19)在全球范围内的大规模传播,PCMR和NVIDIA呼吁全球PC用户加入Folding@home项目贡献自己闲置的GPU计算力,协助抗击新冠状病毒疫情. 目前全球有超过40 ...

  8. 初识ASP.NET CORE

    首先创建一个asp.net core web应用程序 第二步 目前官方预置了7种模板项目供我们选择.从中我们可以看出,既有我们熟悉的MVC.WebAPI,又新添加了Razor Page,以及结合比较流 ...

  9. C# 基础知识系列- 6 Lambda表达式和Linq简单介绍

    前言 C#的lambda和Linq可以说是一大亮点,C#的Lambda无处不在,Linq在数据查询上也有着举足轻重的地位. 那么什么是Linq呢,Linq是 Language Intergrated ...

  10. C语言自学网官方微信相关功能使用方法

    一.微信扫描关注微信公众账号(C语言自学网),我们会不定时更新关于编程的技术文章和相关资讯,了解大佬的成长之路. 二.回复“C语言学习资料”,将显示由我们精心整理的全套C语言学习资料的下载地址及链接. ...