C++ : AVL树-详解

目录

一、AVL树的引入

二、AVL树的高度

三、AVL树的平衡因子

四、AVL树的构造

五、AVL树的insert

[左单旋 :](#左单旋 :)

[右单旋 :](#右单旋 :)

[右左双旋 :](#右左双旋 :)

[左右双旋 :](#左右双旋 :)

六、判断是不是AVL树

七、AVLTree.h


一、AVL树的引入

二叉搜索树虽可以缩短查找的效率,但如果数据有序或接近有序二叉搜索树将退化为单支树

查找元素相当于在顺序表中搜索元素,效率低下

因此,两位俄罗斯的数学家G.M.Adelson-VelskiiE.M.Landis在1962年发明了一种解决上述问题的方法:

当向二叉搜索树中插入新结点后,如果能保证每个结点的左右子树高度之差的绝对值不超过1(需要对树中的结点进行调整),即可降低树的高度,从而减少平均搜索长度

一棵AVL树或者是空树,或者是具有以下性质的二叉搜索树:

  • 它的左右子树都是AVL树
  • 左右子树高度之差(简称平衡因子int _bf)的绝对值不超过1(-1/0/1)

AVL树 : 二叉搜索树 + 每个节点的左右的高度差的绝对值不超过1(-1/0/1)

二、AVL树的高度

满二叉树的高度是log n ,满二叉搜索数的增删查改是log n 那AVL树呢,也是log n,AVL树的两种极端情况

  • 满二叉树的个数 : 2^h-1 = N;
  • AVL树 2^h-x = N ; (1<=x <= 2^(h-1) -1)

三、AVL树的平衡因子

  • 平衡因子本质是一个int类型的变量,它是树的节点的类的一个成员变量 int _bf
cpp 复制代码
template <class K, class V>
struct AVLTreeNode
{
	pair<K, V> _kv;
	AVLTreeNode<K, V>* left;
	AVLTreeNode<K, V>* right;
	AVLTreeNode<K, V>* _parent;
	int _bf;   balance factor

	AVLTreeNode(const pair<K, V>& kv)
		:_kv(kv)
		, left(nullptr)
		, right(nullptr)
		, _parent(nullptr)
		, _bf(0)
	{
	}
};
  • 另外平衡因子是用于判断是否需要进行旋转控制树的高度的,它的计算是采用当前节点的右子树的高度减去左子树的高度就为当前节点的平衡因子
  • 在插入和删除过程中平衡因子需要频繁更新控制:
  1. 新增节点在当前节点的左边,平衡因子--,
  2. 新增节点在当前节点的右边,平衡因子++
  3. 更新后,当前节点的平衡因子等于零,说明当前节点所在指数的高度不变,不会再影响祖先不再继续沿着直到root(根节点)的路径,往上更新
  4. 更新后当前节点的平衡因子等于一或负一,说明当前节点所在子树的高度变化会影响祖先需要继续沿着到root的路径往上更新,
  5. 更新后当前节点(当前节点有可能是上一个节点往上更新的)的平衡因子等于2或-2,说明当前节点所在子树高度变化且不平衡,对当前节点所在子树需要进行旋转,让它平衡

平衡因子指的就是高度差

四、AVL树的构造

节点中的成员由于要经常被访问,于是我们使用struct定义这个模板类

我们使用key_value结构实现,所以用类模板的形式,使用pair键值对存储key_value结构

cpp 复制代码
template <class K, class V>
struct AVLTreeNode

(补充知识 )

  • 键值对 :用来表示具有一一对应关系的一种结构,该结构中一般只包含两个成员变量key和value,key代表键值,value表示与key对应的信息。比如:现在要建立一个英汉互译的字典,那该字典中必然有英文单词与其对应的中文含义,而且,英文单词与其中文含义是一一对应的关系,即通过该应该单词,在词典中就可以找到与其对应的中文含义
  • ps : 说白了不就是 用一个pair 类封装两个可以给定类型的变量 first 和 second ,并且让它们初始化
  • 键值对的定义 :
cpp 复制代码
template <class T1, class T2>
struct pair
{
typedef T1 first_type;
typedef T2 second_type;
T1 first;
T2 second;
pair(): first(T1()), second(T2())
{}
pair(const T1& a, const T2& b): first(a), second(b)
{}};

这里相对于二叉搜索树新增了平衡因子 int _bf , 用于判断AVL树是否需要进行旋转降低树高度,并且还新增了一个parent节点,这个parent节点可以找到当前节点的parent节点 , 即这里的节点的结构是三叉链的结构

cpp 复制代码
struct AVLTreeNode
{
	pair<K, V> _kv;//pair是一个类 ,传k类型和v类型给它,它的类中就包含k first, v second 元素
	AVLTreeNode<K, V>* left;
	AVLTreeNode<K, V>* right;
	AVLTreeNode<K, V>* _parent;
	int _bf;////balance factor
}

接下来实现出这个AVL树的节点的构造函数对成员变量进行初始化即可,初始状态的平衡因子值为0

cpp 复制代码
AVLTreeNode(const pair<K, V>& kv)
	:_kv(kv)
	, left(nullptr)
	, right(nullptr)
	, _parent(nullptr)
	, _bf(0)
{}

接着我们进行AVL树的类模板的初始化,即需要一个root 根节点,但是使用AVL树的节点的类型AVLTreeNode<K, V>定义变量较为繁琐,为了简便将其typedef为Node , 那么我们进行AVL树的构造函数,即进行根节点的初始化,初始化的时候让AVL树的根节点为空即可

(这个AVL树的root根节点的特点是没有parent节点,即规定它的parent节点为空nullptr)

(根节点我们不想让外部访问,所以我们使用访问限定符private限定它为私有成员变量)

cpp 复制代码
template <class K,class V>
class AVLTree
{
	typedef AVLTreeNode<K, V> Node;
public: 
	bool insert(const pair<K, V>& kv);
	
	//通过检查这个树的高度左右子树相不相同
	bool IsBalance(Node* root);

private:
	Node* root = nullptr;
	
	//root 里面有 1.pair<k,V>_kv ,2.parent 节点,3.left 节点,4.right节点
};

五、AVL树的insert

当插入节点的时候,最开始AVL树中没有节点的时候会率先插入AVL树的根节点

(这里 const pair<K, V>& kv 传的是引用 因为 pair是自定义类型,传引用更好,减少拷贝传参)

cpp 复制代码
bool insert(const pair<K, V>& kv)
{
	if (root == nullptr)
	{
		root = new Node(kv);
		return true;
	}
}

在AVL树的平衡因子中我们讲到了

  • 在插入和删除过程中平衡因子需要频繁更新控制:
  1. 新增节点在当前节点的左边,平衡因子--
  2. 新增节点在当前节点的右边,平衡因子++
  3. 更新后,当前节点的平衡因子等于零,说明当前节点所在指数的高度不变,不会再影响祖先不再继续沿着直到root(根节点)的路径,往上更新
  4. 更新后当前节点的平衡因子等于一或负一,说明当前节点所在子树的高度变化会影响祖先需要继续沿着到root的路径往上更新,
  5. 更新后当前节点(当前节点有可能是上一个节点往上更新的)的平衡因子等于2或-2,说明当前节点所在子树高度变化且不平衡,对当前节点所在子树需要进行旋转,让它平衡

那怎么个"往上更新",什么叫对"当前节点所在子树需要进行旋转"

  • 往上更新:当节点的平衡因子是 0 ,不需要往上更新,当 平衡因子不是0 ,是1/-1时

    cpp 复制代码
    else if(parent->_bf==1||parent->_bf==-1)//
    {
    	cur = parent;
    	parent = parent->_parent;
    	// 能走到这里来每一个节点的_parent都已经存着它的parent指针	
    	// else if 走完,会回到while (parent),继续循环,因为cur此时为亚健康会影响它的祖先,它的parent的节点是需要++/--的
    	// 让它到上一个节点去再检查
    }

在一个while循环中往上更新

  • **当前节点所在子树需要进行旋转 :**旋转分为左单旋,右单旋,右左双旋,左右双旋,这四种旋转我们不希望暴露给用户,所以我们将这四种旋转使用private访问限定符设置为私有成员
cpp 复制代码
bool insert(const pair<K, V>& kv)

先找到kv该插入的位置 , parent要存cur的父亲节点

cpp 复制代码
Node* parent = nullptr;
Node* cur = root;//cur  : 找到kv该插入的位置
while (cur)
{
	if (cur->_kv.first < kv.first)
	{
		parent = cur;
		cur = cur->right;
	}
	else if (cur->_kv.first > kv.first)
	{
		parent = cur;
		cur = cur->left;
	}
	else
		return false;
}
cur = new Node(kv);
if (parent->_kv.first < cur->_kv.first)// 一开始写成kv.first 了
{
	parent->right = cur;
}
else
{
	parent->left = cur;
}
cur->_parent = parent;// 也就是说每一个节点的_parent都存着它的parent指针

再该考虑控制平衡

左单旋 :
右单旋 :
cpp 复制代码
	while (parent)//  根节点的parent为空,查找到根节点为止
	{
		if (cur == parent->left)//
		{
			parent->_bf--;
		}
		else
		{
			parent->_bf++;
		}

		if (parent->_bf == 0)// 这棵树已经平衡了,不用往上走了
		{
			break;
		}
	
  • 新增节点 cur 在当前节点 parent 的左边,平衡因子--
  • 新增节点 cur 在当前节点 parent 的右边,平衡因子++

如果parent->_bf == 0 说明这棵树已经平衡了,不用往上走了

cpp 复制代码
	else if(parent->_bf==1||parent->_bf==-1)//
		{
			cur = parent;
			parent = parent->_parent;
			// 能走到这里来每一个节点的_parent都已经存着它的parent指针	
			// else if 走完,会回到while (parent),继续循环,因为cur此时为亚健康会影响它的祖先,它的parent的节点是需要++/--的
			// 让它到上一个节点去再检查
		}	

parent->_bf==1||parent->_bf==-1 继续向上更新

cpp 复制代码
else if(parent->_bf==2||parent->_bf==-2)
		{
			// 子树不平衡了,需要旋转
			// 1. 左单旋
			if (parent->_bf == 2 && cur->_bf == 1)
			{
				Rotatel(parent);
			}
			

Rotatel(parent); 左单旋 :

cpp 复制代码
void Rotatel(Node* parent)
{
	Node* cur = parent->right;
	Node* curleft = cur->left;

	parent->right = curleft;
	if (curleft)//curleft有可能为空
	{
		curleft->_parent = parent;
	}

	cur->left = parent;
	parent->_parent = cur;
	//cur->_parent = parent->_parent;//忽略了parent是ppnode的右边的时候
	Node* ppnode = parent->_parent;//要把parent的祖先给cur,先保存一下
	parent->_parent = cur;

    if (parent == root)//如果parent是根节点,则它的祖先是空
	{
		root = cur;
		//cur->_parent = nullptr;
	}
	else
	{
		if (ppnode->left == parent)
		{
			ppnode->left = cur;
		}
		else
		{
			ppnode->right = cur;
		}
		//cur->_parent = ppnode;
	}
	cur->_parent = ppnode;
	parent->_bf = cur->_bf = 0;
}

	
cpp 复制代码
else if (parent->_bf == -2 && cur->_bf == -1)
			{
				RotateR(parent);
			}
			

RotateR(parent); 右单旋 :

cpp 复制代码
void RotateR(Node* parent)
{
	Node* cur = parent->left;
	Node* curright = cur->right;
	Node* ppnode = parent->_parent;
	parent->left = curright;

	if(curright)//curright有可能为空
	curright->_parent = parent;

	cur->right = parent;
	parent->_parent = cur;
	if (parent == root)
	{
		root = cur;
	}
	else
	{
		if (ppnode->left == parent)
		{
			ppnode->left = cur;
		}
		else
		{
			ppnode->right = cur;
		}
	}
	cur->_parent = ppnode;
	parent->_bf = cur->_bf = 0;

}

其中 该代码的情况

cpp 复制代码
if (ppnode->left == parent)
		{
			ppnode->left = cur;
		}
		else
		{
			ppnode->right = cur;
		}

需要分辨是哪种情况

parent是 ppnode的右边时

parent是 ppnode的左边时

cpp 复制代码
else if (parent->_bf == 2 && cur->_bf == -1)
			{
				RotateRl(parent);
			}
			

RotateRl(parent);

需要右左双旋的情况

在左边的情况时用左单旋,结果得到右边的情况,没有得到我们需要的,所以我们要用到右左双旋

和左右双旋

右左双旋 :
cpp 复制代码
void RotateRl(Node* parent)
{

	Node* cur = parent->right;
	Node* curleft = cur->left;
	int bf = curleft->_bf;

	RotateR(parent->right);
	Rotatel(parent);
    if (bf == 0)
	{
		cur->_bf = 0;
		curleft->_bf = 0;
		parent->_bf = 0;
	}
	else if (bf == 1)
	{
		cur->_bf = 0;
		curleft->_bf = 0;
		parent->_bf = -1;
	}
	else if(bf == -1)
	{
		cur->_bf = 1;
		curleft->_bf = 0;
		parent->_bf = 0;
	}
	else
	{
		assert(false);
	}

}

然后调节平衡因子,注意 在curleft的右边是插入新节点和在curleft的左边是插入新节点是不同的

(下图用的左右双旋的图,当情况是一样的 ,注意看每个节点的平衡因子)

以上是平衡因子的三种可能,

小编用一张图就能交代清楚

(注意 此时用的图是右左双旋的图)

遮住curright的左是curright的平衡因子为1时,遮住curright的右是curright的平衡因子为-1时

左右双旋 :
cpp 复制代码
else if (parent->_bf == -2 && cur->_bf == 1)
			{
				RotatelR(parent);
			}
		}
		

RotatelR(parent);

左右双旋

cpp 复制代码
void RotatelR(Node* parent)
{
	Node* cur = parent->left;
	Node* curright = cur->right;
	int bf = curright->_bf;

	Rotatel(parent->left);
	RotateR(parent);

	if (bf == 0)
	{
		cur->_bf = 0;
		curright->_bf =0;
		parent->_bf = 0;
	}
	else if (bf == 1)
	{
		cur->_bf = -1;
		curright->_bf = 0;
		parent->_bf = 0;
	}
	else if (bf == -1)
	{
		cur->_bf = 0;
		curright->_bf = -1;
		parent->_bf = 0;
	}
	else
	{
		assert(false);
	}
}

在curleft的右边是插入新节点和在curleft的左边是插入新节点是不同的

以上是平衡因子的三种可能

小编认为 ,左右双旋和右左双旋本质一样的,理解了一个,另一个也好理解

cpp 复制代码
else
		{
			assert(false);
		}
	}
	return true;
}

六、判断是不是AVL树

**我们通过检查这个树的高度左右子树相不相同,**来判断这棵树是不是AVL树

cpp 复制代码
bool IsBalance()
{
	IsBalance(root);
}
//判断这个树是不是AVL树
//通过检查这个树的高度左右子树相不相同
bool IsBalance(Node* root)
{
	if (root == nullptr)
	{
		return true;
	}
	int leftHight = Heigh(root->left);
	int rightHight = Height(root->right);

	if (rightHight - leftHight != root->_bf)
	{
		cout << "平衡因子异常" << root->_bf << root->_kv.first << ':'endl;
		return false;
	}
	//abs函数用于计算数值的绝对值
	return abs(rightHight - leftHight) < 2
		&& IsBalance(root->left)
		&& IsBalance(root->right);
}
  • 我们知道AVLTree的最重要的特点就是左右子树的高度差的绝对值不超过1,那么这里我们递归AVL树验证每一层的节点使用高度计算函数Height计算出的右子树的高度减去左子树的高度,即我们计算出并验证每一层的左右子树的高度差的绝对值是否都不超过1,如果不超过那么对应就是AVLTree,否则不是AVLTree
cpp 复制代码
int Height()
{
	return Height(_root);
}

int Height(Node* root)//求二叉树的高度 
{
	if (root == nullptr)
		return 0;

	int leftHeight = Height(root->left);
	int rightHeight = Height(root->right);

	return leftHeight > rightHeight ? leftHeight + 1 : rightHeight + 1;
}
  • 同时我们还可以验证我们算出来当前节点cur的左右子树的高度差Height是否等于当前节点存储的平衡因子 int _bf,因为平衡因子 _bf 就是左右子树的高度差,如果不相等那么打印出"平衡因子异常" 以及 对应节点的key值 ,即_kv中的成员变量first,以及将节点对应的异常的平衡因子 一 一 打印出来,尽管节点中存储的_bf不等于我们计算出来的左右子树的高度差,此时并不代表它不是AVL树,因为有可能节点中存储的_bf不正确,有可能继续递归下去左右子树的高度差的绝对值仍然不超过1符合AVL树的性质,但是由于节点中存储的_bf已经不正确了,此时那么代表平衡因子已经不能作为判断旋转位置AVL树的依据了,所以这里我们返回false

由于递归IsBalance需要控制变量root,但是我们在外部调用这个IsBalance的时候又不能直接的访问AVLTree的根节点_root,这里我们采取子函数的形式进行递归,IsBalance()和IsBalance(Node* root)构成函数重载可以同时存在,同时这里的Height()和Height(Node* root)也是类似的道理

test 一下

cpp 复制代码
#include"AVLTree.h"
int main()
{
	int a[] = { 16,3,7,11,9,26,18,14,15 };
	AVLTree<int, int> t;
	for (auto e : a)
	{
		t.insert(make_pair(e, e));
	}
	return 0;
}
cpp 复制代码
pair<K, V> _kv;//pair是一个类 ,传k类型和v类型给它,它的类中就包含k first, v second 元素

七、AVLTree.h

cpp 复制代码
#pragma once
#include<iostream>
#include<assert.h>
using namespace std;

//template <class K, class V>
//class AVLTreeNode
//{
//public:
//	pair<K, V> _kv;
//	AVLTreeNode<K, V>* left;
//	AVLTreeNode<K, V>* right;
//	AVLTreeNode<K, V>* _parent;
//	int _bf;////balance factor
//
//	AVLTreeNode(const pair<K,V>& kv)
//		:_kv(kv)
//		,left(nullptr)
//		,right(nullptr)
//		,_parent(nullptr)
//		,_bf(0) 
//	{ }
//
//};

template <class K, class V>
struct AVLTreeNode
{
	pair<K, V> _kv;//pair是一个类 ,传k类型和v类型给它,它的类中就包含k first, v second 元素
	AVLTreeNode<K, V>* left;
	AVLTreeNode<K, V>* right;
	AVLTreeNode<K, V>* _parent;
	int _bf;////balance factor

	AVLTreeNode(const pair<K, V>& kv)
		:_kv(kv)
		, left(nullptr)
		, right(nullptr)
		, _parent(nullptr)
		, _bf(0)
	{
	}

};


template <class K,class V>
class AVLTree
{
	typedef AVLTreeNode<K, V> Node;
public: 
	bool insert(const pair<K, V>& kv)
	{
		if (root == nullptr)
		{
			root = new Node(kv);
			return true;
		}

		Node* parent = nullptr;
		Node* cur = root;//cur  : 找到kv该插入的位置
		while (cur)
		{
			if (cur->_kv.first < kv.first)
			{
				parent = cur;
				cur = cur->right;
			}
			else if (cur->_kv.first > kv.first)
			{
				parent = cur;
				cur = cur->left;
			}
			else
				return false;
		}
		cur = new Node(kv);
		if (parent->_kv.first < cur->_kv.first)
		{
			parent->right = cur;
		}
		else
		{
			parent->left = cur;
		}
		cur->_parent = parent;// 也就是说每一个节点的_parent都存着它的parent指针

		// 控制平衡
		  // 更新平衡因子
		while (parent)//  根节点的parent为空,查找到根节点为止
		{
			if (cur == parent->left)//
			{
				parent->_bf--;
			}
			else
			{
				parent->_bf++;
			}

			if (parent->_bf == 0)// 这棵树已经平衡了,不用往上走了
			{
				break;
			}
			else if(parent->_bf==1||parent->_bf==-1)//
			{
				cur = parent;
				parent = parent->_parent;
				// 能走到这里来每一个节点的_parent都已经存着它的parent指针	
				// else if 走完,会回到while (parent),继续循环,因为cur此时为亚健康会影响它的祖先,它的parent的节点是需要++/--的
				// 让它到上一个节点去再检查
			}
			else if(parent->_bf==2||parent->_bf==-2)
			{
				// 子树不平衡了,需要旋转
				// 1. 左单旋
				if (parent->_bf == 2 && cur->_bf == 1)
				{
					Rotatel(parent);
				}
				else if (parent->_bf == -2 && cur->_bf == -1)
				{
					RotateR(parent);
				}
				else if (parent->_bf == 2 && cur->_bf == -1)
				{
					RotateRl(parent);
				}
				else if (parent->_bf == -2 && cur->_bf == 1)
				{
					RotatelR(parent);
				}
				else
				{
					assert(false);
				}
				break;
			}
			else
			{
				assert(false);
			}
		}


		return true;

	}

	void Rotatel(Node* parent)
	{
		Node* cur = parent->right;
		Node* curleft = cur->left;

		parent->right = curleft;
		if (curleft)//curleft有可能为空
		{
			curleft->_parent = parent;
		}

		cur->left = parent;
		parent->_parent = cur;
		//cur->_parent = parent->_parent;//忽略了parent是ppnode的右边的时候
		Node* ppnode = parent->_parent;//要把parent的祖先给cur,先保存一下
		parent->_parent = cur;

		if (parent == root)//如果parent是根节点,则它的祖先是空
		{
			root = cur;
			//cur->_parent = nullptr;
		}
		else
		{
			if (ppnode->left == parent)
			{
				ppnode->left = cur;
			}
			else
			{
				ppnode->right = cur;
			}
			//cur->_parent = ppnode;
		}
		cur->_parent = ppnode;
		parent->_bf = cur->_bf = 0;
	}

	void RotateR(Node* parent)
	{
		Node* cur = parent->left;
		Node* curright = cur->right;
		Node* ppnode = parent->_parent;
		parent->left = curright;

		if(curright)//curright有可能为空
		curright->_parent = parent;

		cur->right = parent;
		parent->_parent = cur;
		if (parent == root)
		{
			root = cur;
		}
		else
		{
			if (ppnode->left == parent)
			{
				ppnode->left = cur;
			}
			else
			{
				ppnode->right = cur;
			}
		}
		cur->_parent = ppnode;
		parent->_bf = cur->_bf = 0;

	}

	void RotateRl(Node* parent)
	{

		Node* cur = parent->right;
		Node* curleft = cur->left;
		int bf = curleft->_bf;

		RotateR(parent->right);
		Rotatel(parent);

		if (bf == 0)
		{
			cur->_bf = 0;
			curleft->_bf = 0;
			parent->_bf = 0;
		}
		else if (bf == 1)
		{
			cur->_bf = 0;
			curleft->_bf = 0;
			parent->_bf = -1;
		}
		else if(bf == -1)
		{
			cur->_bf = 1;
			curleft->_bf = 0;
			parent->_bf = 0;
		}
		else
		{
			assert(false);
		}
	}

	void RotatelR(Node* parent)
	{
		Node* cur = parent->left;
		Node* curright = cur->right;
		int bf = curright->_bf;

		Rotatel(parent->left);
		RotateR(parent);

		if (bf == 0)
		{
			cur->_bf = 0;
			curright->_bf =0;
			parent->_bf = 0;
		}
		else if (bf == 1)
		{
			cur->_bf = -1;
			curright->_bf = 0;
			parent->_bf = 0;
		}
		else if (bf == -1)
		{
			cur->_bf = 0;
			curright->_bf = -1;
			parent->_bf = 0;
		}
		else
		{
			assert(false);
		}
	}
	


	int Height(Node* root)//求二叉树的高度 
	{
		if (root == nullptr)
			return 0;

		int leftHeight = Height(root->left);
		int rightHeight = Height(root->right);

		return leftHeight > rightHeight ? leftHeight + 1 : rightHeight + 1;
	}


	bool IsBalance()
	{
		IsBalance(root);
	}
	//判断这个树是不是AVL树
	//通过检查这个树的高度左右子树相不相同
	bool IsBalance(Node* root)
	{
		if (root == nullptr)
		{
			return true;
		}
		int leftHight = Heigh(root->left);
		int rightHight = Height(root->right);

		if (rightHight - leftHight != root->_bf)
		{
			cout << "平衡因子异常" << root->_bf << endl;
			return false;
		}
		//abs函数用于计算数值的绝对值
		return abs(rightHight - leftHight) < 2
			&& IsBalance(root->left)
			&& IsBalance(root->right);
	}

private:
	Node* root = nullptr;
	
	//root 里面有 1.pair<k,V>_kv ,2.parent ,3.left ,4.right
};

以上就是小编今天的分享

相关推荐
!停2 分钟前
c语言动态申请内存
c语言·开发语言·数据结构
AC赳赳老秦2 分钟前
pbootcms模板后台版权如何修改
java·开发语言·spring boot·postgresql·测试用例·pbootcms·建站
代码or搬砖23 分钟前
Collections和Arrays
java·开发语言
吴名氏.34 分钟前
电子书《Java程序设计与应用开发(第3版)》
java·开发语言·java程序设计与应用开发
于慨1 小时前
dayjs处理时区问题、前端时区问题
开发语言·前端·javascript
listhi5201 小时前
基于MATLAB的LTE系统仿真实现
开发语言·matlab
ss2731 小时前
ScheduledThreadPoolExecutor异常处理
java·开发语言
ejjdhdjdjdjdjjsl1 小时前
Winform初步认识
开发语言·javascript·ecmascript
六毛的毛1 小时前
比较含退格的字符串
开发语言·python·leetcode
xingzhemengyou12 小时前
Python GUI之tkinter-基础控件
开发语言·python