【c++】探究C++中的list:精彩的接口与仿真实现解密

news2024/12/28 4:19:09

Alt

🔥个人主页Quitecoder

🔥专栏c++笔记仓

Alt

朋友们大家好,本篇文章来到list有关部分,这一部分函数与前面的类似,我们简单讲解,重难点在模拟实现时的迭代器有关实现

目录

  • `1.List介绍`
  • `2.接口函数`
    • `operations`
  • `3.模拟实现`
    • `3.1基本框架`
    • `3.2 list的基本函数`
    • `3.3迭代器的封装和实现`
      • `++等重载函数的实现`
      • `与list的关联`
    • `3.4list函数完善`
    • `3.5迭代器进一步完善`
      • `const迭代器`
      • `合并两种迭代器`

1.List介绍

在这里插入图片描述

  1. list是可以在常数范围内在任意位置进行插入和删除的序列式容器,并且该容器可以前后双向迭代
  2. list的底层是双向链表结构,双向链表中每个元素存储在互不相关的独立节点中,在节点中通过指针指向其前一个元素和后一个元素
  3. list与forward_list非常相似:最主要的不同在于forward_list是单链表,只能朝前迭代,已让其更简单高效
  4. 与其他的序列式容器相比(array,vector,deque),list通常在任意位置进行插入、移除元素的执行效率更好
  5. 与其他序列式容器相比,list和forward_list最大的缺陷是不支持任意位置的随机访问,比如:要访问list的第6个元素,必须从已知的位置(比如头部或者尾部)迭代到该位置,在这段位置上迭代需要线性的时间开销;list还需要一些额外的空间,以保存每个节点的相关联信息(对于存储类型较小元素的大list来说这可能是一个重要的因素)

所以list本质就是我们的双向循环链表,我们接下来看它的接口函数

2.接口函数

构造函数
在这里插入图片描述
这里的构造函数与vector类似

  1. Default constructor (构造一个空的 std::list):
std::list<int> myList1;  // 创建一个空的整型链表
  1. Fill constructor (构造一个有特定数量元素且每个元素都有相同初始值的 std::list):
std::list<int> myList2(5, 10); // 创建一个有5个元素的链表,每个元素都初始化为10
  1. Range constructor (从另一个迭代器定义范围的容器中构建 std::list):
std::vector<int> myVector{1, 2, 3, 4, 5};
std::list<int> myList3(myVector.begin(), myVector.end()); // 使用vector的范围来初始化链表
  1. Copy constructor (使用另一个 std::list 来构造一个新的 std::list, 是副本):
std::list<int> myOriginalList{1, 2, 3, 4, 5};
std::list<int> myList4(myOriginalList); // 使用另一个list来初始化这个新的list

每个构造函数都有它们独特的用途,可以根据具体需要选择合适的构造函数进行对象的创建和初始化。

  • 默认构造函数创建一个没有任何元素的空链表。
  • 填充构造函数允许创建一个包含特定数量相同值的元素的链表。
  • 范围构造函数可以从任何提供迭代器接口的其他容器复制元素。
  • 拷贝构造函数创建了一个当前list的副本。

填充构造函数前面的explicit关键字表明这个构造函数不能用于隐式转换或复制初始化,它需要直接调用来构造对象。其他构造函数则根据是否带有explicit关键字来决定是否能用于隐式转换或复制初始化

迭代器

在这里插入图片描述
迭代器用来遍历链表,下面是迭代器的简单使用

list<int> lt = { 10,20,30,40,50 };
list<int>::iterator i1 = lt.begin();
while (i1 != lt.end())
{
	cout << *i1 << " ";
	i1++;
}
cout << endl;
for (auto e : lt)
{
	cout << e << " ";
}
cout << endl;

容量操作

在这里插入图片描述

  • empty检测list是否为空,是返回true,否则返回false
  • size返回有效元素个数的值

元素访问

在这里插入图片描述

  • front返回list的第一个节点值的引用
  • back返回list的最后一个节点值的引用

内容操作
在这里插入图片描述
这里大多数函数我们在前面都讲解过,包括头插头删尾插尾删之类的

insert在这里不会出现迭代器失效

迭代器失效即迭代器所指向的节点的无效,即该节点被删除了。因为list的底层结构为带头结点的双向循环链表,因此在list中进行插入时是不会导致list的迭代器失效的,只有在删除时才会失效,并且失效的只是指向被删除节点的迭代器,其他迭代器不会受到影响

void TestListIterator1()
{
	int array[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 };
	list<int> l(array, array + sizeof(array) / sizeof(array[0]));
	auto it = l.begin();
	while (it != l.end())
	{
		l.erase(it);
		++it;
	}
}

erase()函数执行后,it所指向的节点已被删除,因此it无效,在下一次使用it时,必须先给其赋值

修改:

void TestListIterator()
{
	int array[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 };
	list<int> l(array, array + sizeof(array) / sizeof(array[0]));
	auto it = l.begin();
	while (it != l.end())
	{
		l.erase(it++); // it = l.erase(it);
	}
}
l.erase(it++);

这一行做了以下几件事情:

  1. it++ 表达式返回 it 的当前值(我们来称它为“旧迭代器”),然后将 it 自增,指向下一个元素(现在 it 指向“新迭代器”)。
  2. l.erase(旧迭代器) 调用删除了旧迭代器当前指向的元素,并使这个旧迭代器失效。
  3. 因为 it 已经自增,它现在指向原来被删除元素的下一个元素,因此循环可以继续。

请注意对于 std::list,这个行为是有效的,因为 erase 不会影响除了被删除元素之外的任何迭代器。但如果是其他类型的容器,如 std::vectorstd::deque 中使用相同的技巧就可能会出问题,因为这些容器的 erase 操作可能会导致所有指向被删除元素之后元素的迭代器全部失效。因此,应谨慎地使用这种技术,并且要确认你了解容器的迭代器失效规则。

operations

在这里插入图片描述
std::list 提供了一些有用的成员函数,允许执行各种操作,如元素的合并、移除、排序和倒序。下面是这些函数的简要说明和使用示例:

  1. splice: 将元素从一个列表转移到另一个列表,可以转移整个列表、一个单独的元素或一个元素范围。不会执行任何元素的复制或者移动操作,只是在内部节点之间调整指针。
std::list<int> list1 = {1, 2, 3};
std::list<int> list2 = {4, 5, 6};
list1.splice(list1.end(), list2);  // 把list2的所有元素移动到list1的末尾
  1. remove: 从列表中移除所有具有特定值的元素。
std::list<int> myList = {1, 2, 3, 3, 4, 3, 5};
myList.remove(3);  // 移除所有值为3的元素
  1. remove_if: 根据一个判断条件移除元素。
std::list<int> myList = {1, 2, 3, 4, 5};
myList.remove_if([](int n){ return n % 2 == 0; });  // 移除myList中所有偶数元素
  1. unique: 移除连续并且重复的元素,只保留唯一的元素。
std::list<int> myList = {1, 1, 2, 2, 2, 3, 4, 4, 5, 5};
myList.unique();  // 移除相邻重复的元素,列表变为1, 2, 3, 4, 5
  1. merge: 合并两个已排序的列表,并确保结果列表也是排序的。
std::list<int> list1 = {1, 3, 5};
std::list<int> list2 = {2, 4, 6};
list1.merge(list2);  // 合并两个列表为1, 2, 3, 4, 5, 6
  1. sort: 对列表中的元素进行排序。它接受一个比较函数作为参数(可选)。
std::list<int> myList = {4, 3, 5, 2, 1};
myList.sort();  // 排序列表为1, 2, 3, 4, 5
  1. reverse: 反转列表中元素的顺序。
std::list<int> myList = {1, 2, 3, 4, 5};
myList.reverse();  // 反转后列表为5, 4, 3, 2, 1

这些操作与 std::list 的双向链表特性和内部实现密切相关。例如,splice 不产生元素复制,因为链表中的节点可以简单地重新链接。对于排序操作,std::list 提供了特定的成员函数 sort 以优化排序算法,因为链表不支持随机访问,标准的排序算法(如 std::sort)不适用。

3.模拟实现

3.1基本框架

namespace own {
    template<class T>
    struct ListNode
    {
        ListNode<T>* _next;
        ListNode<T>* _prev;
        T _data;

        ListNode(const T&x=T())
            :_next(nullptr)
            , _prev(nullptr)
            , _data(x)
        {}
    };
    template<class T>
    class list 
    {
        typedef ListNode<T> Node;
    private:
        Node* _head;
        size_t _size;
    };
}

这段代码实现了一个简单双向链表的基础结构。让我们分两部分来解释这个代码:

  1. namespace own命名空间 own 用于封装代码,避免与其他库中的同名类型或函数冲突。在这个命名空间中,定义了模板类 ListNode 和模板类 list

  2. 模板类 ListNode 的定义:

    template<class T>
    struct ListNode {
        ListNode<T>* _next;
        ListNode<T>* _prev;
        T _data;
    
        ListNode(const T& x=T())
            : _next(nullptr), _prev(nullptr), _data(x) {}
    };
    

    这是一个结构体模板,它定义了双向链表的一个节点。每个 ListNode 包含三个成员:

    • _next 指向下一个 ListNode 的指针
    • _prev 指向前一个 ListNode 的指针
    • _data 存储节点的数据,其类型为模板参数 T

    ListNode 还有一个构造函数,它接受一个 const T& 类型的参数,如果构造函数没有提供参数,则会使用 T 类型的默认构造函数来初始化 _data。构造函数还将 _next_prev 初始化为 nullptr,表示链表的下一个和前一个节点分别不存在

  3. 模板类 list 的定义:

    template<class T>
    class list {
        typedef ListNode<T> Node;
    private:
        Node* _head;
        size_t _size;
    };
    

    这个类表示双向链表本身:

    • 类型定义 typedef ListNode<T> Node 是为了简化代码,使得在类 list 中可以直接使用 Node 来指代 ListNode<T>
    • _head 是一个指向链表头部节点的指针。
    • _size 是一个 size_t 类型的值,用来存储链表的长度(即节点个数)。

3.2 list的基本函数

🔥list()

list()
{
    _head = new Node;
    _head->_next = _head;
    _head->_prev = _head;
    _size = 0;
}

带头双向循环链表,我们初始化时让它的头结点的前一个指针和后一个指针指向自身

🔥尾插push_back()

void push_back(const T& x)
{
    Node* newnode = new Node(x);
    Node* tail = _head->_prev;
    tail->_next = newnode;
    newnode->_prev = tail;
    newnode->_next = _head;
    _head->_prev = newnode;
    _size++;
}

在这里插入图片描述

  • 获取尾节点,即head的上一个位置
  • 更新四个指针即可

那我们完成了尾插工作,接下来如何实现下面的遍历呢?

void test1()
{
    list<int> lt;
    lt.push_back(1);
    lt.push_back(2);
    lt.push_back(3);
    lt.push_back(4);
    lt.push_front(6);
    list<int>::iterator it = lt.begin();
    while (it != lt.end())
    {
        cout << *it << " ";
        ++it;
    }
    cout << endl;
}

这部分来到本篇的重点,迭代器的封装和实现

3.3迭代器的封装和实现

我们思考一下,这里原生指针能否代替迭代器?

在这里插入图片描述
我这里的指针,只能是Node*Node*++不会加到下一个节点,这里的++需要我们自己重载实现,解引用也取不到当前节点的位置,这些函数都需要我们自己重载实现,所以,我们需要对迭代器进行封装

template<class T>
struct ListIterator
{
    typedef ListNode<T> Node;
    Node* _node;
    ListIterator(Node* node)
        :_node(node)
    {}
};

注意,我们迭代器实在list类域访问的:

list<int>::iterator it = lt.begin();

这里我们就需要在list类里面进行嵌套

template<class T>
class list {
public:
    // 这是一个嵌套类型的别名定义。
    typedef ListIterator<T> iterator;
    // ...
};

list<int>::iterator it = lt.begin(); 这一行涉及到了所谓的嵌套类型或者内嵌类型

在C++中,当一个类型(比如 ListIterator<T>)是在另一个类型的作用域内部定义的(比如 list<T>)时,这个类型被称为嵌套类型。嵌套类型通常用于与外部类型紧密相关联的概念,例如迭代器、节点或其他辅助类。

list 类中定义了 iterator 类型作为 ListIterator<T> 类型的别名:

template<class T>
class list {
public:  
    typedef ListIterator<T> iterator;
};

这里的 iteratorlist 类的嵌套类型的别名,所以当我们在类外部引用它时,我们需要使用类的名称(在这个例子中是 list<int>),后跟两个冒号来限定作用域,然后是别名 iterator因此 list<int>::iterator 指的是 ListIterator<int>它是链接到特定的 list 实例化类型的迭代器类型

要点是,内嵌类型通常在类定义内部,并且与该类紧密关联。当我们在类外部谈到这些类型时,需要使用类的名称来限定这些类型,就像我们引用 list<int>::iterator 一样。这种设计方式提供了良好的封装和组织结构,在集合和容器类(如 list)中是一种常见做法

迭代器就是一个节点的指针,我们这个类的成员就是_node(节点指针)

 typedef ListNode<T> Node;
    Node* _node;
    ListIterator(Node* node)
        :_node(node)
    {}

这里用一个节点的指针就可以对迭代器进行初始化

++等重载函数的实现

这个封装类的关键就是对这进行重载函数

🔥operator++()与operator- -()

这里++加到下一个节点,改变节点指针:

前置++

typedef ListIterator<T> Self
Self& operator++()
{
    _node = _node->_next;
    return *this;
}

后置++

Self operator++(int)
{
    Self tmp(*this);
    _node = _node->_next;
    return tmp;
}

拷贝构造一份,返回++之前的值

这里迭代器我们没有写拷贝构造,调用默认的拷贝构造进行浅拷贝,这里的浅拷贝是可以的,因为我的期望就是两个指针都只向同一个节点

Self& operator--()
{
    _node = _node->_prev;
    return *this;
}

Self operator--(int)
{
    Self tmp(*this);
    _node = _node->_prev;

    return tmp;
}

🔥operator*()

这里解引用得到节点的值,需要我们自己手动重载实现:

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

这里返回的是引用值,是对原数据可直接修改的

🔥operator!=()

我们在循环中还有判断两个迭代器不相等,本质就是两个节点指针不相等

bool operator!=(const Self& it)
{
    return _node != it._node;
}

基本完成类的封装后,我们需要把它和list连起来

与list的关联

在这里插入图片描述
这里begin和end只能由list提供

template<class T>
class list {
    typedef ListNode<T> Node;
public:
    typedef ListIterator<T> iterator;

    iterator begin()
    {
        return iterator(_head->_next);//匿名对象
    }
    iterator end()
    {
        return iterator(_head);//最后一个位置的下一个位置
    }
    ''''''''''''''''''''''''''''''''''''
    //其他函数
    private:
    Node* _head;
    size_t _size;
};

begin返回第一个数据的迭代器end返回最后一个数据的下一个位置

测试代码:

void test1()
{
    list<int> lt;
    lt.push_back(1);
    lt.push_back(2);
    lt.push_back(3);
    lt.push_back(4);
    list<int>::iterator it = lt.begin();
    while (it != lt.end())
    {
        cout << *it << " ";
        ++it;
    }
    cout << endl;
}

在这里插入图片描述

由于我的外部代码使用了iterator别名,这里typedef ListIterator<T> iterator;必须放在public下

3.4list函数完善

有了迭代器,我们就可以完善其他的list函数

🔥insert()

void insert(iterator pos, const T& val)
{
    Node* cur = pos._node;//拿到这个位置的节点
    Node* newnode = new Node(val);
    Node* prev = cur->_prev;
    prev->_next = newnode;
    newnode->_prev = prev;
    newnode->_next = cur;
    cur->_prev = newnode;
    _size++;
}

🔥erase()

iterator erase(iterator pos)
{
    Node* cur = pos._node;
    Node* prev = cur->_prev;
    Node* next = cur->_next;

    prev->_next = next;
    next->_prev = prev;
    delete cur;
    --_size;
    return iterator(next);
}

erase删除返回的是下一个位置的迭代器

🔥头尾删插

有了insert和erase,我们这几个函数就可以直接进行复用

void push_front(const T& x)
{
    insert(begin(), x);
}
void push_back(const T& x)
{
    insert(end(), x);
}
void pop_back()
{
    erase(--end());
}
void pop_front()
{
    erase(begin());
}

与size相关函数:

size_t size()const
{
    return _size;
}
bool empty()
{
    return _size == 0;
}

🔥链表销毁

void clear()
{
    iterator it = begin();
    while (it != end())
    {
        it = erase(it);
    }
}
~list()
{
    clear();
    delete _head;
    _head = nullptr;
}

3.5迭代器进一步完善

对于下面的这种类

 struct A
 {
     int _a1;
     int _a2;

     A(int a1 = 0, int a2 = 0)
         :_a1(a1)
         , _a2(a2)
     {}
 };

如果list存的是上面的自定义类型呢?

插入有以下几种方法:

void test_list2()
{
    list<A> lt;
    A aa1(1, 1);
    A aa2 = { 1, 1 };
    lt.push_back(aa1);
    lt.push_back(aa2);
    lt.push_back(A(2, 2));
    lt.push_back({ 3, 3 });
    lt.push_back({ 4, 4 });  
}

上面代码使用不同方式来创建和插入 A 类型的对象到自定义的 list 容器中。下面是每种方式的详细说明以及它们所涉及的概念:

  1. 有名对象的直接插入:

    A aa1(1, 1);
    lt.push_back(aa1);
    

    这里,首先创建了一个命名对象 aa1,使用了 A 的构造函数 A(int a1, int a2) 并为其提供了两个参数。然后,你将 aa1 作为参数传递给 lt.push_back() 函数。在这种情况下,aa1 是有名对象(也就是说,它有一个名称),并且 push_back 函数接受到的是 aa1 的一个副本

  2. 多参数隐式类型转换:

    A aa2 = { 1, 1 };
    lt.push_back(aa2);
    

    aa2 通过列表初始化的方式被创建。这里的列表初始化允许直接用花括号 {} 来初始化对象。C++11 引入的列表初始化特性可以用来初始化任何对象,包括具有构造函数的对象。创建了 aa2 有名对象并将其插入到列表中

  3. 通过构造函数创建匿名对象并插入:

    lt.push_back(A(2, 2));
    

    在这里,没有给新创建的 A 对象一个名字,因此它是一个匿名对象(也称作临时对象)。这个匿名的 A 对象是通过调用它的构造函数来直接初始化的,并立即被传递到 push_back 函数中。

  4. 通过隐式类型转换创建匿名对象并插入:

    lt.push_back({ 3, 3 });
    

    与第三种方式类似,创隐式类型转换建了一个匿名的 A 对象,但这次是通过。初始化时没有使用相应类型的构造函数,而是依赖编译器生成的代码来创建一个具有给定初始化列表的对象,并将其传递给 push_back 函数。

在所有这些情况中,实际插入到 list 容器中的都是 A

现在我们来进行遍历:

list<A>::iterator it = lt.begin();
while (it != lt.end())
{
    cout<<*it<<" ";
    ++it;
}

在这里插入图片描述
A是自定义类型,不支持留插入,我们解引用得到的_data是A的对象

这里数据是公有的,解引用得到的可以通过.访问符进行访问

cout << (*it)._a1 << ":" << (*it)._a2 << endl;

这种访问方式相当于这种形式:

A* ptr = &aa1;
(*ptr)._a1;

这种指针访问行为十分复杂,我们可以重载一个函数使实现这种访问方式

ptr->_a1;

在迭代器中重载->运算符

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

这里返回的是_data的地址

我们就可以这样访问:

cout << it->_a1 << ":" << it->_a2 << endl;

实际上它的访问方式如下:

cout << it.operator->()->_a1 << ":" << it.operator->()->_a2 << endl;

注意

这里隐藏了一个箭头,一个是重载,一个是原生指针的访问操作

实际上,并不需要在代码中写两次箭头。这是因为在 C++ 中,operator-> 有一个特殊的规则

当重载 operator->,不会直接返回成员的值,而是应该返回一个指针,这个指针指向的对象包含我们想要访问的成员。当使用 ->运算符时,C++ 会自动和透明地调用重载的 operator-> 并继续 “链式” 访问成员,而不需要程序员显示地添加多余的箭头。

这是如何工作的:

  1. 如果有一个用户自定义类型的对象(比如迭代器)it,并且我们调用 it->member,编译器会查找这个类型是否有 operator->
  2. 如果这个类型有一个 operator-> 的重载,编译器就调用 it.operator->()
  3. it.operator->() 应该返回一个指向对象的指针,这个对象有一个我们尝试访问的 member 成员
  4. 编译器取得这个指针,并继续访问 member

这个流程只需要一个 -> 即可。编译器在背后处理了所有操作,直到访问到一个实际对象,然后这个对象的成员可以直接通过 -> 运算符访问

因此,我们不需要手动写成 it->->member;只需写 it->member 即可。编译器会自动将其 “转换” 为 it.operator->()->member

ListIterator 示例里,it->_a1 意味着:

  1. 调用 it.operator->() 拿到 ListNode_data 成员的地址(这是一个 A 类型的对象)。
  2. 使用返回的指针来访问 A 对象的 _a1 成员。

整个过程对于编程者来说是透明的,不需要编写多个 ->。这种处理方式使得重载 -> 可以更自然地使用,就像处理普通的指针一样

const迭代器

我们上面写的迭代器对于const对象是无法编译成功的,const不能调用非const成员函数

对于const类迭代器,我们需要在list类里面重新增加重载

typedef ConstListIterator<T> const_iterator;

const_iterator begin() const
{
    return _head->_next;
}

const_iterator end() const
{
    return _head;
}

使普通版本调用普通版本,const调用const版本

这里的const迭代器不能是下面这种形式,而需单独创建版本:

const iterator end() const
{
    return _head;
}

因为const迭代器的本质是迭代器指向的内容不能修改,而不是迭代器本身不能修改

const iterator这样定义是迭代器不能修改,内容还是可以修改的

那我们如何实现const迭代器呢?

方法一:单独实现一个类,修改正常版本的迭代器

template<class T>
struct ConstListIterator
{
    typedef ListNode<T> Node;
    typedef ConstListIterator<T> Self;
    Node* _node;
    ListIterator(Node* node)
        :_node(node)
    {}
    const T* operator->()
    {
        return &_node->_data;
    }
    const T& operator*()
    {
        return _node->_data;
    }
    Self& operator++()
    {
        _node = _node->_next;
        return *this;
    }
    Self operator++(int)
    {
        Self tmp(*this);
        _node = _node->_next;
        return tmp;
    }
    Self& operator--()
    {
        _node = _node->_prev;
        return *this;
    }

    Self operator--(int)
    {
        Self tmp(*this);
        _node = _node->_prev;

        return tmp;
    }
    
    bool operator!=(const Self& it)
    {
        return _node != it._node;
    }

};

我们目的是不修改指向的值,只需要在这两个函数前面加上const即可:

const T* operator->()
{
    return &_node->_data;
}
const T& operator*()
{
    return _node->_data;
}

在这里插入图片描述
我们这两个类只有这两个函数不同,那么能不能将他们两个合并呢?

合并两种迭代器

这里仅是两种返回类型不同,这里我们利用模版来对这里内容进行合并

template<class T, class Ref, class Ptr>
	struct ListIterator
	{
		typedef ListNode<T> Node;
		typedef ListIterator<T, Ref, Ptr> Self;
		Node* _node;
		ListIterator(Node* node)
			:_node(node)
		{}
		// *it
		//T& operator*()
		Ref operator*()
		{
			return _node->_data;
		}
		// it->
		//T* operator->()
		Ptr operator->()
		{
			return &_node->_data;
		}
	};

我们只提取不同的部分,其他部分与原来相同

Ref代表引用,Ptr代表指针

让我们来看一下这个合并后的迭代器的模板参数:

  • T列表节点存储的数据类型
  • Ref通过迭代器访问数据时的返回类型,可以是T&或者const T&
  • Ptr通过迭代器访问数据的指针类型,可以是T*或者const T*

这样,我们可以创建一个常量迭代器,为RefPtr参数指定常量类型,例如:

ListIterator<T, const T&, const T*> const_iterator;

对于非常量迭代器,就简单地传递非常量类型的引用和指针:

ListIterator<T, T&, T*> iterator;

list类中,我们需要相应地声明两种类型的迭代器:

template<class T>
class list {
    // ... 省略其他代码 ...

public:
    typedef ListIterator<T, T&, T*> iterator;
    typedef ListIterator<T, const T&, const T*> const_iterator;

    // ... 省略其他代码 ...
};

list类中的其他成员函数像beginend需要按照是否接收常量类型来适配这两种迭代器。

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

CSS3新增特性(二)

四、2D 转换 • 属性名&#xff1a;transform &#xff08;可用于制作2D转换&#xff0c;也可用于制作3D转转换&#xff1b;2D转换是平面上的转换&#xff0c;3D转换是在三维立体空间的转换&#xff09; • 作用&#xff1a;对元素进行水平或垂直方向的移动、缩放、旋转、拉长…

你们项目日志是如何处理的???

ELK日志采集系统 1.什么是ELK ELK 是一套流行的数据搜索、分析和可视化解决方案&#xff0c;由三个开源项目组成&#xff0c;每个项目的首字母合起来形成了“ELK”这一术语&#xff1a; Elasticsearch (ES): Elasticsearch 是一个基于 Apache Lucene 构建的分布式、实时搜索与…

【Python性能优化】list、array与set

list、array与set 详述测试代码 详述 本文对比 list 与 set 在插入和取值时的性能差异&#xff0c;以提供一条什么时候该选择什么数据类型的建议。先上结果&#xff1a; array 与 list 的不同&#xff1a; 内存方面 array 是 C array 的包装&#xff0c;它直接存储数据&#xf…

Vu3+QuaggaJs实现web页面识别条形码

一、什么是QuaggaJs QuaggaJS是一个基于JavaScript的开源图像识别库&#xff0c;可用于识别条形码。 QuaggaJs的作用主要体现在以下几个方面&#xff1a; 实时图像处理与识别&#xff1a;QuaggaJs是一款基于JavaScript的开源库&#xff0c;它允许在Web浏览器中实现实时的图像…

授权协议OAuth 2.0之通过OIDC实现SSO

写在前面 本文来一起看下OIDC&#xff08;openid connect&#xff09;相关内容。 1&#xff1a;什么是OIDC OIDC的全称是openid connect&#xff0c;和OAuth2.0一样&#xff0c;也是属于协议和规范的范畴。OAuth2.0是一种授权协议&#xff0c;即规定了what you can do的内容…

kafka部分partition的leader=-1修复方案整理

kafka部分partition的leader-1修复方案整理 1. 背景说明2. 修复测试2.1 创建正常的topic并验证生产和消费2.2 停止kafka模拟leader-12.3 修复parition2.4 修复完成验证生产消费是否恢复 3. 疑问和思考3.1 kafka在进行数据消费时&#xff0c;如果有partition的leader-1&#xff…

MySQL基础之多表操作(多表查询,事务,索引)

目录 一、多表关系1.1 一对多1.2 外键约束1.3 一对一1.4 多对多 二、多表查询2.1 测试数据准备2.2 笛卡尔积2.3 内连接2.4 外连接2.5 子查询1.标量子查询2.列子查询3.行子查询4.表子查询 三、事务3.1 问题场景引入3.2 概念3.3 事务操作3.4 事务的四大特性ACID 四、索引4.1 概念…

人工智能高速发展 苹果能否赶上AI热潮

近年来&#xff0c;人工智能高速发展&#xff0c;科技大佬们竞相布局人工智能赛道。苹果虽然“晚”了一步&#xff0c;但也在不断追赶这股热潮。随着AI技术不断发展&#xff0c;这项技术已经成为手机发展的新风向标&#xff0c;甚至2024年被视为AI手机元年。 众多手机厂商铆足了…

vue做导入导出excel文档

系统中经常会遇到要实现批量导入/导出数据的功能&#xff0c;导入就需要先下载一个模板&#xff0c;然后在模板文件中填写内容&#xff0c;最后导入模板&#xff0c;导出就可能是下载一个excel文件。 1、导出 新建一个export.js文件如下&#xff1a; import {MessageBox,Mes…

【Xilinx】时序约束学习 TIMING-2: 基准时钟源管脚无效

在错误的管脚 <pin_name> 上创建了基准时钟 <clock_name>。建议仅在适当的时钟根&#xff08;不含时序弧的输入端口或原语输出管脚&#xff09; 上创建基准时钟。 描述 基准时钟必须在时钟树的源时钟上定义。例如&#xff0c; 源时钟可能是设计的输入端口。如果在…

4.20.1 深度神经网络提高放射科医生在乳腺癌筛查中的表现

新颖的两阶段神经网络&#xff0c;用于将全局和局部信息与适当的训练过程结合起来。这使我们能够使用非常高容量的块级网络从像素级标签中学习&#xff0c;同时网络也可以从宏观乳房级标签中学习。模型可以生成可解释的热图&#xff0c;指示可疑发现的位置。即使在拥有大量图像…

Neo4j数据库(二)

目录 WHERE子句 简单WHERE子句语法 复杂WHERE子句语法 DELETE删除 删除节点 DELETE节点子句语法 REMOVE删除 删除节点/关系的属性 REMOVE属性子句语法 SET子句 SET子句语法 WHERE子句 像SQL一样&#xff0c;Neo4j CQL在CQL MATCH命令中提供了WHERE子句来过滤MATCH查询…

VMware-Linux切换桥接模式上网教程(超详细)

这里写目录标题 1. 虚拟机关机2. VMware 虚拟网络配置2.1 检查是否存在 VMnet02.2 修改桥接模式2.3 修改Linux虚拟机网络适配器 3. Linux 系统配置3.1 修改系统网卡配置3.1.1 配置项含义解释3.1.2 查看物理机网络信息3.3.3 修改配置 3.2 重启服务 4. 测试网络连接情况5. 注意事…

Oracle Analytics BIEE 操作方法(四)仪表盘导出“区域”不换行也不合并居中的方法

1 分析 取消所有区域内列格式里面的换行 2 仪表盘 点击“工具” 打印和导出选项 设置固定列宽&#xff08;范围是&#xff1a;0-300&#xff09;

图片像素高效处理,轻松将图片像素进行按比例缩小50%并保存在指定位置,让您的图像更精致!

图像与我们的日常生活紧密相连&#xff0c;从社交媒体分享到专业摄影作品展示&#xff0c;高质量的图片像素处理显得至关重要。然而&#xff0c;面对海量的图片数据和高分辨率的图像处理需求&#xff0c;如何高效、简便地进行像素调整成为了众多用户关注的焦点。 第一步&#…

怎么通过PHP语言实现远程控制门锁

怎么通过PHP语言实现远程控制门锁呢&#xff1f; 本文描述了使用PHP语言调用HTTP接口&#xff0c;实现控制门锁&#xff0c;通过智能出门开关&#xff0c;配合普通的电插锁、电磁锁&#xff0c;来实现门禁控制。 可选用产品&#xff1a;可根据实际场景需求&#xff0c;选择对应…

有时候知道自己该去做什么,但就是不去行动,怎么办?

只要有想法&#xff0c;马上就去办&#xff01;马上、及时、立刻去办&#xff01;这可能是唯一解决这个问题的方法&#xff0c;您办完事情之后的感觉会有极强的满足感&#xff0c;然后再逐步慢慢的开始您所有的计划安排&#xff0c;一定会水到渠成&#xff01;您可能不信&#…

4.3网安学习第四阶段第三周回顾(个人学习记录使用)

本周重点 ①Java入门基础 ②Java方法入门 ③Java面向对象(**重点) ④Java的Maven构建项目工具 ⑤Java其他操作补充 ⑥Java序列化和反序列化 ⑦Url 的DNS 审计链(**难点) ⑧Springboot和前端数据分离(主流使用) ⑨Common Collections(CC链漏洞) 本周主要内容 ①Java入…

GDPU unity游戏开发 碰撞体与关节

让你设计的角色跑起来吧&#xff0c;可以是动画&#xff0c;也可以是碰撞器的运动。 运动小车 找到小车素材&#xff0c;导入到层级面板。然后可以新建一个地面让小车在上面运动&#xff0c;新建一个方块当障碍物。 摆放好后&#xff0c;要加组件。记住&#xff0c;在unity中运…

算法刷题day47

目录 引言一、滑动窗口二、单调栈三、繁忙的都市 引言 今天把所有的最短路算法和最小生成树算法又给复习了一下&#xff0c;有 s p f a , 朴素版 d i j k s t r a , 堆优化版 d i j k s t r a , f l o y d , p r i m , k r u s k a l spfa,朴素版dijkstra,堆优化版dijkstra,f…