C++ set map 的模拟实现

在这里插入图片描述

set 的模拟实现

我们在很早之前就提到过,set 的底层数据结构是红黑树。红黑树的实现一般都是 key-value 的结构。但是我们在使用 set 的时候明明只传入了一个模板参数哇!我们来看库中的实现:

在这里插入图片描述

我们可以看到,set 的模板参数 Key 就是存储的数据类型,库中对 Key 进行了重命名:key_type 和 value_type,然后将 key_type 和 value_type 分别作为红黑树的 key 和 value。因此 set 的底层就是存储了两个 key 的红黑树。

我们仿照库里面的实现方式,就可以定义出来我们自己的 set 的结构啦:

#pragma once
#include"RBTree.h"

namespace Tchey
{
    template<class K>
    class set
    {
    public:


    private:
        RBTree<K, K> _t;
    };
}

记得复制一份我们自己实现的红黑树哦!

修改红黑树节点的定义

为了使得一个红黑树的模板类能同时适配出来 map 和 set 我们需要对红黑树节点的定义做修改!

在上面的 set 类中成员属性是这么写的:RBTree<K, K> _t。在使用 map 的时候我们知道 map 的数据是以 key-value 的形式存储的。那 map 类中成员属性该怎么写呢?是这样吗:RBTree<K, V> _t。我们先来看我们之前实现的红黑树对于节点的定义:

template<class K, class V>
struct RBTreeNode
{
	RBTreeNode<K, V>* _left;
	RBTreeNode<K, V>* _right;
	RBTreeNode<K, V>* _parent;

	pair<K, V> _kv;
	Color _col; //节点的颜色

	RBTreeNode(const pair<K, V>& kv)
		:_left(nullptr)
		,_right(nullptr)
		,_parent(nullptr)
		,_kv(kv)
		,_col(RED) //节点默认的颜色是红色
	{}
};

这是以 key-value 的形式定义的红黑树节点。如果 map 就是上面那样定义的话,那么节点中的 K 和 V 都是存储的数据。

  • 当我们在实现红黑树的迭代器的时候,重载 * 运算符的返回值就很难书写,对于 set 返回 K 就行,但是对于 map 就必须返回 pair<K, V> 无法做到统一,那么意味着你就需要对 set 写一个红黑树迭代器的实现,map 写一个红黑树迭代器的实现。就会有大量重复的代码,这是不被容忍的!
  • 其次这么写在 set 插入数据的时候就必须插入一个 pair<K, K> 导致红黑树中的节点中存储了冗余的数据。

因此我们就需要对红黑树的节点稍作修改,使得一个红黑树就能适配出 map 和 set。在我们之前实现其他容器的时候我们已经积累了相关的经验,只要不一样,直接将不一样的地方提炼成模板参数就行啦,根据传入参数的不同,实例化出来不同的类型。

于是我们对红黑树节点做出如下修改:

template<class T>
struct RBTreeNode
{
	RBTreeNode<T>* _left;
	RBTreeNode<T>* _right;
	RBTreeNode<T>* _parent;

	T _data;
	Color _col; //节点的颜色

	RBTreeNode(const T& data)
		:_left(nullptr)
		,_right(nullptr)
		,_parent(nullptr)
		,_data(data)
		,_col(RED) //节点默认的颜色是红色
	{}
};

修改红黑树的成员属性

RBTree 的实现中,成员变量是红黑树的根节点,当时我们是这样定义的:

template<class K, class V>
class RBTree
{
    typedef RBTreeNode<K, V> Node;
    Node* _root;
}

现在我们修改了红黑树节点的定义自然不能这么写了!联想到 set 中是这么使用红黑树的:RBTree<K, K> _t 看上去定义 Node 既可以传 K 也可以传 V。但是为了适配 map,我们会选择传递 V 过去,像下面这样:

template<class K, class V>
class RBTree
{
    typedef RBTreeNode<V> Node;
    Node* _root;
}

改到这里,想必你就明白了 map 中的红黑树应该怎么定义了:真正的数据类型是 RBTree 的第二个模板参数,因此 map 中应该这么使用红黑树:RBTree<K, pair<K, V>> _t

通过这样的方式构造出来的红黑树节点,里面的数据既不会冗余,也可以做到同时适配 map 和 set。

迭代器实现

红黑树,节点的物理空间不连续,无法使用原生指针作为红黑树的迭代器,需要对节点进行封装,构造一个迭代器类!同 list 的迭代器,因为我们不仅要实现普通的迭代器,还需要实现 const 的迭代器,所以还需要加上两个模板参数,用来控制 operator*operator-> 返回不同的值。

下面就是最基本的结构啦:

template<class T, class Ref, class Ptr>
struct __TreeIterator
{
	typedef RBTreeNodeL<T> Node;
	typedef __TreeIterator<T, Ref, Ptr> self;

	Node* _node;
};

构造函数

在等会实现 begin,end 等接口时都需要构造迭代器返回。迭代器的构造显然是通过节点的指针来的!

template<class T, class Ref, class Ptr>
struct __TreeIterator
{
	typedef RBTreeNodeL<T> Node;
	typedef __TreeIterator<T, Ref, Ptr> self;
	__TreeIterator(Node* node)
		:_node(node)
	{}

	Node* _node;
};

Ref operator*() 和 Ptr operator->()

有了前面的铺垫,我们知道 operator*operator-> 是可以直接适配 map 和 set 的,直接上手写就行啦。

Ref operator*()
{
    return _node->_data;
}

Ptr operator->()
{
    return &_node->_data;
}

bool operator!=(const self& s) const

判断两个迭代器是不是不想等,就是判断节点的指针是否相等!

bool operator!=(const self& s) const
{
    return _node != s._node;
}

self& operator++()

我们通过例子来看看 operator++() 的逻辑是怎么样的?在 set 的学习部分我们知道了,使用迭代器遍历 set 之后得到的是一个有序的序列,显然迭代器遍历的顺序就是走的中序遍历。

如下图所示的例子:iterator 对应的节点是 13 那么加加之后显然就是 15。我们来抽一下,假设 15 是一颗红黑树,而不是单纯的一个节点!那么 13 这个节点对应的迭代器加加之后,就是右子树对应的最左节点。

在这里插入图片描述

经过这么抽象,我们就得到了 operator++ 的关键操作之一:如果当前迭代器对应的节点的右子树不为空,那么加加之后的结果就是右子树的最小节点(右子树的最左节点)。

那么如果右子树为空怎么办呢?

我们来看下面的情况,iterator 对应的节点值为 22,那么这个位置的迭代器加加之后显然就是 25 这个节点对应的迭代器。根据这个现象,如果当前 iterator 对应节点的右子树为空,那么就是父节点对应位置的迭代器嘛?

在这里插入图片描述

答案是不完全是哦!我们来看下面的情况:

这个 iterator 对应的节点为 15,那么加加之后该指向哪个节点呢?是 13 吗?根据中序遍历的结果来看,加加之后正确的结果应该是 17 这个节点的迭代器嘛!

在这里插入图片描述

显然,当右子树为空的时候,父节点不一定是当前位置的迭代器加加之后的结果。只有当 当前节点位于父节点的左侧的时候,加加的结果才是父节点对应的迭代器。如果当前节点位于父节点的右侧,那么就要向上跟新 cur(当前节点) 和 parent(父节点) 直到 cur(当前节点) 位于 parent(父节点) 的左侧,此时的父节点就是加加之后的结果。

Self& operator++()
{
    if (_node->_right)
    {
        // 右树的最左节点(最小节点)
        Node* subLeft = _node->_right;
        while (subLeft->_left)
        {
            subLeft = subLeft->_left;
        }

        _node = subLeft;
    }
    else
    {
        Node* cur = _node;
        Node* parent = cur->_parent;
        // 找孩子是父亲左的那个祖先节点,就是下一个要访问的节点
        while (parent && cur == parent->_right)
        {
            cur = cur->_parent;
            parent = parent->_parent;	
        }

        _node = parent;
    }

    return *this;
}

self& operator–()

减减的逻辑和加加的逻辑差不多!减减就是先判断左子树是不是为 nullptr,如果不为空那么就找到左子树的最右节点(最大节点)。要是当前节点的左子树为空:如果当前节点位于父节点的右侧,那么父节点就是减减之后的结果;如果当前节点位于父节点的左侧,那么就需要向上更新 cur(当前节点) 和 parent(父节点) 直到 cur 位于 parent 的右侧,此时 parent 对应位置的迭代器就是减减之后的结果。

Self& operator--()
{
    if (_node->_left) //如果左子树不为空,找到左子树的最大节点(最右节点)
    {
        Node* subRight = _node->_left;
        while (subRight->_right)
        {
            subRight = subRight->_right;
        }

        _node = subRight;
    }
    else
    {
        // 当前节点的左子树为空
        Node* cur = _node;
        Node* parent = cur->_parent;
        //找到cur 位于 parent 右侧的那个节点,这个 parent 就是减减之后的结果
        while (parent && cur == parent->_left)
        {
            cur = cur->_parent;
            parent = parent->_parent;
        }
        _node = parent;
    }

    return *this;
}

红黑树的 begin()

begin 返回的迭代器就是整颗红黑树中最小的那个节点,也就是整颗红黑树中最左侧的那个节点。

iterator begin()
{
    Node* cur = _root;
    while(cur && cur->_left)
    {
        cur = cur->_left;
    }
    return cur; //单参数的构造函数支持隐士类型转化
}

//这是 const 的版本
const_iterator end() const
{
    return nullptr;
}

红黑树的 end()

end 返回的迭代器可以是用 nullptr 构造出来的迭代器。

iterator end()
{
    return nullptr;
}

const_iterator end() const
{
    return nullptr;
}

set 的 begin 和 end

还记得我们在 set 的使用部分讲的,set 中的元素为什么不能被修改嘛。因为无论是普通的迭代器还是 const 迭代器,本质上都是红黑树的 const 迭代器,所以通过迭代器来修改 set 中的值是不现实的。根据这个逻辑,我们自己就能实现 set 的 begin 和 end 啦。

iterator begin()
{
    return _t.begin();
}

iterator end()
{
    return _t.end();
}

const_iterator begin() const
{
    return _t.begin();
}

const_iterator end() const
{
    return _t.end();
}

因为在 set 中无论是 iterator 还是 const_iterator 实际上都是 const_iterator,所以不加 const 版本的 begin()end() 可以不写的。

红黑树的 pair<iterator, bool> Insert(const T& data)

我们之前实现的红黑树 insert 的返回值都是 bool 类型的,我们需要在每一个返回的位置都进行处理。

第一个要处理的位置:寻找插入节点的比较方式。之前的比较是已经确定了插入的数据是一个 pair,因此用插入数据的 first 和 节点存储的 pair 的 first 比较就可以确定插入位置。

在这里插入图片描述

然而,为了同时适配 map 和 set 我们的存储类型已经变成了一个模板参数 T。对于 set,红黑树节点存储 的数据类型 T 就是实例化 set 是传入的那个一个类型;对于 map,红黑树节点存储的数据类型就是一个 pair,因此在比较的时候就没有办法做到统一,怎么解决呢?

答案就是仿函数!!!

我们可以给 RBTree 增加一个模板参数 KeyOfT,来获取到不同 T 对应的 key 值。你可能会说 RBTree 不是有一个 K 的模板参数嘛,能直接用到这里嘛,当然是不能的哦!K 仅仅是一个类型,不是节点存储的数据哦!

在这里插入图片描述

我们来看看 set 怎么实例化红黑树的:

template<class K>
class set
{
    struct SetKeyOfT
    {
        const K& operator()(const K& key)
        {
            return key;
        }
    };
public:

private:
    RBTree<K, K, SetKeyOfT> _t;
};

调用 operator() 的时候,根据如果传入的是 K 类型的数据,直接返回传入的 key 就行啦。但是如果传入的是 pair<K, V> 类型的数据,那么就需要返回传入数据的 first 这才是我们比较需要的 Key 值。这个处理逻辑就是 map 的啦!

template<class K, class V>
class map
{
    struct MapKeyOfT
    {
        const K& operator()(const pair<K, V>& kv)
        {
            return kv.first;
        }
    };
public:

private:
    RBTree<K, pair<const K, V>, MapKeyOfT> _t;
};

处理之后的结果就是这样啦,接下来我们只需要在 set 的实现中复用红黑树的 Insert 接口就行啦。

template<class K, class T, class KeyOfT>
class RBTree
{
	typedef RBTreeNode<T> Node;
public:
	typedef __TreeIterator<T, T&, T*> iterator;
	typedef __TreeIterator<T, const T&, const T*> const_iterator;

public:
	
	iterator begin()
	{
		Node* cur = _root;
		while(cur && cur->_left)
		{
			cur = cur->_left;
		}
		return cur; //单参数的构造函数支持隐士类型转化
	}

	const_iterator begin() const
	{
		Node* cur = _root;
		while(cur && cur->_left)
		{
			cur = cur->_left;
		}
		return cur; //单参数的构造函数支持隐士类型转化
	}

	iterator end()
	{
		return nullptr;
	}

	const_iterator end() const
	{
		return nullptr;
	}

	
	pair<iterator, bool> Insert(const T& data)
	{
		//插入的时候如果根节点为 nullptr, 申请一个节点,将该节点的颜色改为黑色之后作为根节点就行啦
		if (_root == nullptr)
		{
			_root = new Node(data);
			_root->_col = BLACK;
			return make_pair(iterator(_root), true);
		}
		//记录父节点
		Node* parent = nullptr;
		Node* cur = _root;
		KeyOfT kot; //实例化一个对象用来调用 operator()
		while (cur) //找到新节点的插入位置
		{
			if (kot(cur->_data) < kot(data)) //新插入的节点比根节点大,与右子树的根节点比较
			{
				parent = cur;
				cur = cur->_right;
			}
			else if (kot(cur->_data) > kot(data)) //新插入的节点比根节点小,与左子树的根节点比较
			{
				parent = cur;
				cur = cur->_left;
			}
			else //如果有相同的节点,插入失败
			{
				return make_pair(iterator(cur), false);
			}
		}
		//申请新的节点
		cur = new Node(data);

		Node* newNode = cur; //记录一下新插入的节点,方便构造 

		//解耦操作
		cur->_col = RED;
		//确定新的节点插入到那个位置,左孩子还是右孩子
		if (kot(parent->_data) < kot(data))
		{
			parent->_right = cur;
		}
		else
		{
			parent->_left = cur;
		}
		//向上链接父节点
		cur->_parent = parent;

		//调整节点的颜色,使之满足红黑树的性质,只有 parent 的颜色为红才会调整节点的颜色,parent 为黑就直接完成插入了嘛
		while (parent && parent->_col == RED)
		{
			//祖父节点
			Node* grandfather = parent->_parent;
			//这里根据父节点在祖父节点的位置进行分类,因为我们要确定 uncle 的位置嘛,这样分类代码比较简洁
			//parent 位于 grandparent 的左侧
			if (parent == grandfather->_left)
			{
				Node* uncle = grandfather->_right;
				// uncle 存在且为红
				if (uncle && uncle->_col == RED)
				{
					// 变色
					parent->_col = uncle->_col = BLACK;
					grandfather->_col = RED;

					// 继续向上处理
					cur = grandfather;
					parent = cur->_parent;
				}
				else // uncle 不存在 或 uncle 存在且为黑
				{
					if (cur == parent->_left)
					{
						//     g
						//   p
						// c
						//右单旋
						RotateR(grandfather);
						parent->_col = BLACK;
						grandfather->_col = RED;
					}
					else
					{
						//     g
						//   p
						//		c
						//左右双旋
						RotateL(parent);
						RotateR(grandfather);

						cur->_col = BLACK;
						grandfather->_col = RED;
					}

					break; //一旦经过旋转调整颜色之后就一定满足红黑树的性质了,直接结束循环
				}
			}
			else // parent == grandfather->_right
			{
				//找到叔叔节点
				Node* uncle = grandfather->_left;
				// uncle 存在且为红
				if (uncle && uncle->_col == RED)
				{
					// 变色
					parent->_col = uncle->_col = BLACK;
					grandfather->_col = RED;

					// 继续向上处理
					cur = grandfather;
					parent = cur->_parent;
				}
				else
				{
					if (cur == parent->_right)
					{
						// g
						//	  p
						//       c
						//左单旋
						RotateL(grandfather);
						grandfather->_col = RED;
						parent->_col = BLACK;
					}
					else
					{
						// g
						//	  p
						// c
						//右左双旭那
						RotateR(parent);
						RotateL(grandfather);
						//调整颜色
						cur->_col = BLACK;
						grandfather->_col = RED;
					}

					break; //一旦经过旋转调整颜色之后就一定满足红黑树的性质了,直接结束循环
				}
			}
		}

		_root->_col = BLACK; // 无论根节点的颜色是否在调整的过程中变成了红色,最后我们都将根节点的颜色变为黑色,方便写代码

		return make_pair(iterator(newNode), true);
	}
}

set 的 insert()

你可能会说,set 里面的 insert 直接复用红黑树里面的 insert 不就行了吗?我们直接来试试:

在这里插入图片描述

我们发现无法编译通过,说什么无法转化。这是什么原因呢?其实就是 set 的 iterator 有问题。因为 set 的 iterator 就是 const_iterator 调用红黑树里面的 insert 接口,返回的是一个普通的迭代器,而 set 中的insert 返回的实际上是一个 const_iterator。虽然红黑树中的 iterator 与 const_iterator 来自与同一个类模板,但是因为传入模板参数的不同,压根就是两个不同的类型,不能转化实属正常。

你会怎么解决这个问题呢?可以将红黑树中的 insert 的返回值改成 const_iterator 嘛?这么改的确解决了 set 的问题。map 怎么办?要是 map 调用红黑树中的 insert 得到的就是个 const_iterator,但是 map 需要的是 iterator 哇!这种办法行不通。

看来就只能提供一个 iterator 到 const_iterator 的转换函数啦!怎么实现呢?

template<class T, class Ref, class Ptr>
struct __TreeIterator
{
	typedef RBTreeNode<T> Node;
	typedef __TreeIterator<T, Ref, Ptr> self;
	__TreeIterator(Node* node)
		:_node(node)
	{}

	typedef __TreeIterator<T, T&, T*> iterator;

	__TreeIterator(const iterator& it)
		:_node(it._node)
	{}
}

我们增加了一个看上去很像拷贝构造的构造函数。__TreeIterator 中的 iterator 模板参数的类型是 __TreeIterator<T, T&, T*>。说明这个 iterator 无论模板参数 RefPtr 传入什么都是非 const 的iterator。当 RefPtr 分别传入 T&T* ,那么 __TreeIterator(const iterator& it) 就是一个拷贝构造函数,因为 iterator 与类实例化出来的对象时一个类型嘛;当 RefPtr 分别传入 const T&const T* ,那么 __TreeIterator(const iterator& it) 就是一个将非 const 的迭代器转化为 const 迭代器的构造函数。是不是相当美妙。

通过添加这个看上去十分像拷贝构造的函数,就可以实现同时适配 map 和 set 啦!
在这里插入图片描述

测试通过,set 模拟实现完成。

map 的模拟实现

在 set 的模拟实现部分,我们已经将红黑树改成同时适配 map 和 set 的结构了,因此 map 的实现直接调用红黑树对应的接口就好啦!

V& operator[]

map 的使用部分浅浅的讲过。opertor[] 就是先调用 insert 函数,如果说插入成功返回插入成功的节点对应的 value 值就可以;如果插入失败,返回与新插入节点 key 值相同的那个节点对应的 value 值就行。这恰好根 insert 的返回值对应上了,我们只需要返回 insert 的返回值中 first 迭代器对应节点的 value 值就大功告成了。

V& operator[](const K& key)
{
    pair<iterator, bool> ret = insert(make_pair(key, V()));
    return ret.first->second;
}

map 的代码

#pragma once
#include"RBTree.h"

namespace Tchey
{
    template<class K, class V>
    class map
    {
        struct MapKeyOfT
        {
            const K& operator()(const pair<K, V>& kv)
            {
                return kv.first;
            }
        };
    
    typedef typename RBTree<K, pair<const K, V>, MapKeyOfT>::iterator iterator;
    typedef typename RBTree<K, pair<const K, V>, MapKeyOfT>::const_iterator const_iterator;

    public:
        iterator begin()
        {
            return _t.begin();
        }

        iterator end()
        {
            return _t.end();
        }

        const_iterator begin() const
        {
            return _t.begin();
        }

        const_iterator end() const
        {
            return _t.end();
        }

        pair<iterator, bool> insert(const pair<K, V>& kv)
        {
            return _t.Insert(kv);
        }
        
        V& operator[](const K& key)
        {
            pair<iterator, bool> ret = insert(make_pair(key, V()));
            return ret.first->second;
        }

    private:
        RBTree<K, pair<const K, V>, MapKeyOfT> _t;
    };
}

测试:

在这里插入图片描述

到这里 map 和 set 的模拟实现就完成啦,还是比较有意思的,对吧。​😜

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/130067.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

深入理解强化学习——多臂赌博机:乐观初始值

分类目录&#xff1a;《深入理解强化学习》总目录 目前为止我们讨论的所有方法都在一定程度上依赖于初始动作值 Q 1 ( a ) Q_1(a) Q1​(a)的选择。从统计学角度来说&#xff0c;这些方法&#xff08;由于初始估计值&#xff09;是有偏的。对于采样平均法来说&#xff0c;当所有…

Linux编译器——gcc/g++使用

前言&#xff1a; 在上一篇&#xff0c;我们学习了关于文本编辑器 vim 的全部知识&#xff0c;今天给大家带来的是关于Linux编译器—gcc/使用的详细介绍。 本文目录 &#xff08;一&#xff09;温习程序的产生的过程 1、前言 2、程序的产生过程 3、&#x1f31c;初步认识 gc…

中兴新支点国产系统将联合阿里龙蜥社区制定多项行业标准

近日&#xff0c;从中兴新支点操作系统官方了解到&#xff0c;中兴迎来阿里龙蜥社区理事长马涛一行人&#xff0c;并进行了深度交流。会上中兴新支点操作系统回顾了近一年在龙蜥社区的贡献和投入&#xff0c;并对双方未来的合作诉求和合作计划展开了讨论。会后&#xff0c;龙蜥…

代理IP的稳定性至关重要!真实技术案例引发深思

在当今的网络世界中&#xff0c;IP代理已经成为了我们保护个人隐私、开展各类互联网业务的重要工具。不过很多人在使用IP代理时&#xff0c;常常会忽视一个关键因素——代理IP的稳定性。今天我们就来谈谈这个问题&#xff0c;并分享一个真实的案例&#xff0c;希望能引起大家对…

linux粘滞位的介绍及使用

文章目录 1.粘滞位的引入2.粘滞位的使用 1.粘滞位的引入 首先看一个场景 已知 对目录无w权限时 无法进行目录中的文件的创建/删除操作但是普通用户通过sudo命令 以root身份创建一个文件 rw- r-- r-- 普通用户此时是other 没有w权限 但却可以删除 [root和普通用户在一个目录下时…

1210. 连号区间数(枚举)

题目&#xff1a; 1210. 连号区间数 - AcWing题库 思路&#xff1a;枚举 枚举一般是先暴力再优化。 注意&#xff1a;对于区间的枚举&#xff0c;一般是定一移一。固定任何一端移动另外一端均可以。但是此题为便于在枚举移动端的过程中确定最大最小&#xff0c;选择定左移右…

TinyMce富文本编辑器使用【详细】

TinyMCE是一款易用、且功能强大的所见即所得的富文本编辑器。同类程序有&#xff1a;UEditor、Kindeditor、Simditor、CKEditor、wangEditor、Suneditor、froala等等。 TinyMCE的优势&#xff1a; 开源可商用&#xff0c;基于LGPL2.1插件丰富&#xff0c;自带插件基本涵盖日常…

css3文字环绕旋转

目录 固定数量文字环绕旋转不固定数量文字环绕旋转效果图 固定数量文字环绕旋转 <!-- 文字旋转测试 --> <template><div class"page"><div><div v-for"(item, index) in [...Array(20).keys()]" :key"index" style&…

104.c语言中的define的两个模糊点

1. define 是按照从上到下的顺序的 #define 必须先定义&#xff0c;否则报错 2.函数体内的define的影响 2.1 从定义开始的位置起&#xff0c;之后都有效 不受函数作用域的限制 #include <stdio.h>//int a[N] {0};#define N 100int a[N] {0}; //int b[X]; void abcd(v…

高等数学教材重难点题型总结(三)函数与极限

首先是考研大纲包含的内容&#xff1a; 1.理解并会用罗尔(Rolle)定理、拉格朗日(Lagrange)中值定理和泰勒(Taylor)定理&#xff0c;了解并会用柯西(Cauchy)中值定理. 2.掌握用洛必达法则求未定式极限的方法. 3.理解函数的极值概念&#xff0c;掌握用导数判断函数的单调性和求函…

基于SSM的在线作业管理系统 -octopus-master(源码+调试)

项目描述 临近学期结束&#xff0c;还是毕业设计&#xff0c;你还在做java程序网络编程&#xff0c;期末作业&#xff0c;老师的作业要求觉得大了吗?不知道毕业设计该怎么办?网页功能的数量是否太多?没有合适的类型或系统?等等。这里根据疫情当下&#xff0c;你想解决的问…

虚幻5 删除C盘缓存及修改缓存路径

一.修改C盘缓存 C盘缓存路径为&#xff1a; C:\Users\xx(这里是你的用户名)\AppData\Local\UnrealEngine\Common\DerivedDataCache 注意&#xff0c;如果没有AppData文件夹&#xff0c;请依次点击查看-勾选显示隐藏的项目&#xff0c;即可 可删除里面的所有文件即可 二.修改…

leetcode 刷题 - 有效三角形个数 - 长度最小的子数组 - 无重复字符的最长子串

l611. 有效三角形的个数 - 力扣&#xff08;LeetCode&#xff09; 给定一个包含非负整数的数组 nums &#xff0c;返回其中可以组成三角形三条边的三元组个数。 示例 1:输入: nums [2,2,3,4] 输出: 3 解释:有效的组合是: 2,3,4 (使用第一个 2) 2,3,4 (使用第二个 2) 2,2,3示…

【Git】Git的GUI图形化工具ssh协议IDEA集成Git

一、GIT的GUI图形化工具 1、介绍 Git自带的GUI工具&#xff0c;主界面中各个按钮的意思基本与界面文字一致&#xff0c;与git的命令差别不大。在了解自己所做的操作情况下&#xff0c;各个功能点开看下就知道是怎么操作的。即使不了解&#xff0c;只要不做push操作&#xff0c;…

天翼云江西分公司副总经理彭越华一行莅临拓世科技集团指导考察,共绘蓝图开启智能新篇章

世界经济脉络在数字化的浪潮中迎来了新的生机&#xff0c;企业的成长轨迹正在智能化的力量下重新塑造。天翼云科技有限公司江西分公司副总经理彭越华一行的到访&#xff0c;为拓世科技集团带来了新的发展机遇。这场深入的交流&#xff0c;不仅预示着在科技创新和数字化转型的征…

【漏洞复现】BYTEVALUE智能流控路由器存在命令执行

【漏洞介绍】 百为智能流控路由器 /goform/webRead/open 路由的 ?path 参数存在有回显的命令注入漏洞。攻击者可通过该漏洞在服务器端执行命令&#xff0c;写入后门&#xff0c;获取服务器权限&#xff0c;从而获取路由器权限。 【指纹】 title”BYTEVALUE 智能流控路由器”…

Electron-vue出现GET http://localhost:9080/__webpack_hmr net::ERR_ABORTED解决方案

GET http://localhost:9080/__webpack_hmr net::ERR_ABORTED解决方案 使用版本解决方案解决总结 使用版本 以下是我解决此问题时使用的electron和vue等的一些版本信息 【附】经过测试 electron 的版本为 13.1.4 时也能解决 解决方案 将项目下的 .electron-vue/dev-runner.js…

Node版本管理工具——Nvm

文章目录 前言基础常识彼此之间的关系 一、安装 nvm&#xff1f;查看是否安装成功 二、配置下载源三、nvm常用命令 前言 nvm 全名 node.js version management&#xff0c;顾名思义是一个nodejs的版本管理工具。通过它可以安装和切换不同版本的nodejs。 基础常识 node&#x…

Java时间工具类:ZTDateTimeUtil

目录 1.返回指定格式的当前时间,Date-->FormatString,Date类型转Strig 2.返回固定格式的Date类型时间Date---》ToString---》ToDate,Date类型格式化成Date 3.字符串转日期 String格式化成String 4.两时间关系判断构件 5.Date转换为字符串:Date格式化成String 6.String类…

【canvas】在Vue3+ts中实现 canva内的矩形拖动操作。

前言 canvas内的显示内容如何拖动&#xff1f; 这里提供一个 canvas内矩形移动的解决思路。 描述 如何选中canvas里的某部分矩形内容&#xff0c;然后进行拖动&#xff1f; 我的解决思路&#xff1a; **画布搭建。**用一个div将canvas元素包裹&#xff0c;设置宽高&#xf…