深入剖析LinkedList:揭秘底层原理

在这里插入图片描述

文章目录

    • 一、 概述LinkedList
      • 1.1 LinkedList简介
      • 1.2 LinkedList的优点和缺点
    • 二、 LinkedList数据结构分析
      • 2.1 Node节点结构体解析
      • 2.2 LinkedList实现了双向链表的原因
      • 2.3 LinkedList如何实现了链表的基本操作(增删改查)
      • 2.4 LinkedList的遍历方式
    • 三、 源码分析
      • 3.1 成员变量
      • 3.2 构造方法
      • 3.3 add()方法
      • 3.4 remove()方法
      • 3.5 get()方法
      • 3.6 set()方法
      • 3.7 clear()方法
      • 3.8 indexOf()方法
    • 四、 总结及实战应用
      • 4.1 LinkedList适用场景
      • 4.2 LinkedList与ArrayList的比较
      • 4.3 LinkedList的使用注意事项
      • 4.4 实战应用:设计一个基于LinkedList的LRU缓存算法

一、 概述LinkedList

1.1 LinkedList简介

LinkedList是Java中的一种双向链表数据结构实现类,它实现了ListDeque接口。

LinkedList的特点主要包括以下几点

  1. 链表结构:LinkedList内部使用链表来存储元素,每个节点都包含当前元素的值以及指向前一个节点和后一个节点的引用。这种链表结构使得插入和删除元素的操作效率较高。
  2. 双向访问:每个节点都有指向前一个节点和后一个节点的引用,这使得在LinkedList中可以通过前向或后向遍历访问元素,而不需要像ArrayList那样进行元素的整体移动。
  3. 动态大小:LinkedList没有固定的容量限制,可以根据需要动态地添加或删除元素,并且不会出现数组扩容的情况。
  4. 随机访问较慢:由于LinkedList是基于链表的数据结构,因此在访问特定索引位置的元素时,需要从头或尾部开始遍历到目标位置。相比之下,ArrayList可以通过索引直接访问元素,因此在随机访问元素时效率更高。
  5. 支持队列和栈操作:作为Deque接口的实现类,LinkedList可以被用作队列(先进先出)和栈(后进先出)的数据结构,可以使用addFirst、removeFirst等方法模拟栈操作,使用addLast、removeFirst等方法模拟队列操作。

总体而言,LinkedList适用于频繁地插入和删除元素的场景,但对于随机访问元素的需求不是很高时,可以考虑使用它。

1.2 LinkedList的优点和缺点

优点

  1. 链表的插入和删除操作比较快速,因为只需要改变指针指向即可。
  2. 可以在任意位置进行插入和删除操作,而不需要像数组那样需要移动其他元素。
  3. 在迭代时可以快速获取下一个元素,因为每个节点都有指向前后节点的指针。

缺点

  1. 链表的访问操作比较慢,因为需要遍历整个链表才能找到对应的元素。
  2. 由于链表每个节点都需要额外存储前后节点的指针,因此占用的内存空间比数组大。
  3. 链表没有像数组那样可以直接访问任意位置的元素,因此不能使用索引随机访问元素。
  4. 当需要频繁进行随机访问时,由于缺乏连续的内存空间,可能会导致缓存命中率降低,从而影响性能。

综上所述,LinkedList 适合在需要频繁进行插入和删除操作,但是不需要频繁随机访问元素的场景下使用。如果需要随机访问或者占用空间比较重要时,可以考虑使用其他数据结构,比如 ArrayList

二、 LinkedList数据结构分析

2.1 Node节点结构体解析

在Java中,LinkedList的实现是通过一个双向链表来实现的。在LinkedList中,每个节点都有一个指向前一个节点和一个指向后一个节点的引用。

在LinkedList中,Node节点结构体可以被定义如下

private static class Node<E> {E item;Node<E> next;Node<E> prev;Node(E element, Node<E> next, Node<E> prev) {this.item = element;this.next = next;this.prev = prev;}
}

上面的代码定义了一个私有静态内部类Node,该类包含了一个泛型元素item,以及两个Node类型的引用nextprev,分别表示指向下一个节点和上一个节点的引用。此外,Node类还有一个带有三个参数的构造函数,用于初始化节点的prev、itemnext属性。

通过这样的设计,LinkedList可以通过Node节点来构建双向链表结构,实现了在任意位置进行节点的插入和删除操作。

2.2 LinkedList实现了双向链表的原因

Java中的LinkedList实现了双向链表是因为双向链表具有以下优点

  1. 双向链表可以从前向后或从后向前遍历,而单向链表只能从前往后遍历,这使得双向链表更加灵活和高效。
  2. 双向链表能够提供O(1)时间复杂度的前驱和后继节点访问操作,而单向链表则需要O(n)时间复杂度的遍历操作才能找到前驱节点。
  3. 双向链表相对于单向链表来说,其插入和删除节点的操作更加高效,因为只需要改变前后节点的指针指向即可,而单向链表需要先找到前驱节点再进行操作。

因此,双向链表在实现一些需要频繁插入和删除操作的场景下,比单向链表更加适用。Java中的LinkedList就是基于双向链表实现的,所以它能够高效地支持插入、删除等操作,同时也提供了迭代器和列表迭代器等许多方便的操作。

2.3 LinkedList如何实现了链表的基本操作(增删改查)

在Java中,LinkedList类实现了链表的基本操作,包括增加(添加)、删除、修改和查询等功能。下面是一些常用的方法:

  1. 添加操作:
    • addFirst(E e):在链表的开头添加元素。
    • addLast(E e):在链表的末尾添加元素。
    • add(int index, E element):在指定位置插入元素。
  2. 删除操作
    • removeFirst():删除链表的第一个元素。
    • removeLast():删除链表的最后一个元素。
    • remove(int index):删除指定位置的元素。
    • remove(Object o):删除指定元素。
  3. 修改操作
    • set(int index, E element):将指定位置的元素替换为新的元素。
  4. 查询操作
    • getFirst():返回链表的第一个元素。
    • getLast():返回链表的最后一个元素。
    • get(int index):返回指定位置的元素。
    • indexOf(Object o):返回指定元素在链表中的索引位置。
    • contains(Object o):判断链表是否包含指定元素。

LinkedList类还提供了其他一些方法用于获取链表的大小、清空链表、判断链表是否为空等。

2.4 LinkedList的遍历方式

在Java中,你可以使用以下几种方式对LinkedList进行遍历:

1.使用迭代器(Iterator)进行遍历

LinkedList<String> linkedList = new LinkedList<>();
// 假设已经向linkedList中添加了元素
Iterator<String> iterator = linkedList.iterator();
while (iterator.hasNext()) {String element = iterator.next();// 对element进行处理
}

2.使用增强型for循环(foreach)进行遍历

LinkedList<String> linkedList = new LinkedList<>();
// 假设已经向linkedList中添加了元素
for (String element : linkedList) {// 对element进行处理
}

这两种方式都可以用来遍历LinkedList中的元素,你可以根据实际情况选择其中一种来进行遍历操作。

三、 源码分析

3.1 成员变量

在这里插入图片描述

3.2 构造方法

/*** 默认构造函数,它不带任何参数,用来创建一个空的 LinkedList 对象*/
public LinkedList() {
}/*** 带有参数的构造函数,它接受一个类型为 Collection 的参数 c*/
public LinkedList(Collection<? extends E> c) {// 调用无参构造函数this();// 将参数 c 中的所有元素添加到新创建的 LinkedList 对象中addAll(c);
}/*** LinkedList 类中的 addAll 方法的定义*/
public boolean addAll(Collection<? extends E> c) {// size表示为当前 LinkedList 中的元素数量// 调用另一个名为 addAll 的方法来将参数 c 中的所有元素添加到 LinkedList 对象中return addAll(size, c);
}/*** 用于将指定集合中的元素插入到指定位置*/
public boolean addAll(int index, Collection<? extends E> c) {// 检查索引的有效性,确保索引在范围内checkPositionIndex(index);// 将集合 c 转换为数组,并将其赋值给对象数组 aObject[] a = c.toArray();// 获取新添加元素的数量int numNew = a.length;// 如果新添加元素的数量为 0,则直接返回 falseif (numNew == 0)return false;// 定义两个节点,pred 表示当前节点的前一个节点,succ 表示当前节点的后一个节点LinkedList.Node<E> pred, succ;// 根据插入位置确定 succ 和 pred 的值// 如果插入位置在末尾,则将 succ 设置为 null,将 pred 设置为最后一个节点if (index == size) {succ = null;pred = last;} else {// 否则,通过 node(index) 方法找到指定位置的节点,并将其设置为 succ,同时将其前一个节点设置为 predsucc = node(index);pred = succ.prev;}// 遍历数组 a 中的元素for (Object o : a) {@SuppressWarnings("unchecked") E e = (E) o;// 将每个元素转换为泛型类型 E,并创建一个新的节点 newNode,该节点的前一个节点为 pred,值为 e,后一个节点为 nullLinkedList.Node<E> newNode = new LinkedList.Node<>(pred, e, null);// 如果 pred 为 null,则将 newNode 设置为链表的第一个节点if (pred == null)first = newNode;else// 否则,将 pred 的下一个节点设置为 newNodepred.next = newNode;// 将 pred 更新为 newNodepred = newNode;}// 根据 succ 是否为 null 来确定插入后的链表结构// 如果 succ 为 null,则表示插入位置在末尾,将 pred 设置为最后一个节点if (succ == null) {last = pred;} else {// 否则,将 pred 的下一个节点设置为 succ,并将 succ 的前一个节点设置为 predpred.next = succ;succ.prev = pred;}// 更新链表的大小和修改计数器size += numNew;modCount++;// 返回 true,表示添加操作成功完成return true;
}

3.3 add()方法

/*** 用于向链表末尾添加一个元素 e*/
public boolean add(E e) {// 将元素 e 添加到链表的末尾linkLast(e);// 添加操作成功return true;
}/*** 向链表末尾添加一个元素的操作*/
void linkLast(E e) {// 创建一个名为 l 的局部变量,用于保存当前链表的最后一个节点的引用,通过访问 last 字段获取最后一个节点的引用final LinkedList.Node<E> l = last;// 创建一个新的节点 newNode,并将其初始化为一个具有前驱节点为 l、元素为 e、后继节点为 null 的节点final LinkedList.Node<E> newNode = new LinkedList.Node<>(l, e, null);// 将链表的 last 指针指向新的节点 newNode,使其成为最后一个节点last = newNode;// 如果 l 为空(即链表为空),则将链表的 first 指针指向新的节点 newNode,否则将 l 节点的后继节点指向新的节点 newNodeif (l == null)first = newNode;elsel.next = newNode;// 增加链表的大小,将链表中元素的数量加1size++;// 增加修改计数器 modCount 的值,用于追踪链表结构的修改次数modCount++;
}

3.4 remove()方法

/*** 用于从 LinkedList 中删除指定的元素*/
public boolean remove(Object o) {// 判断传入的参数 o 是否为 null// 如果 o 为 null,则说明要删除的元素是 null 值if (o == null) {// 因此需要遍历 LinkedList 中的所有节点并查找 item 属性为 null 的节点// 循环体中的变量 x 表示当前正在遍历的节点,初始化为 first(即头节点),在每次迭代后更新为下一个节点,直到遍历完整个 LinkedListfor (LinkedList.Node<E> x = first; x != null; x = x.next) {// 如果当前节点的 item 属性为 null(在 o 为 null 的情况下),则调用辅助方法 unlink 删除该节点并返回 trueif (x.item == null) {unlink(x);return true;}}} else {// 否则需要遍历所有节点并查找 item 属性等于 o 的节点for (LinkedList.Node<E> x = first; x != null; x = x.next) {// 如果当前节点的 item 属性等于 o(在 o 不为 null 的情况下),则调用辅助方法 unlink 删除该节点并返回 trueif (o.equals(x.item)) {unlink(x);return true;}}}return false;
}/*** 这是一个辅助方法,用于删除指定节点 x*/
E unlink(LinkedList.Node<E> x) {// assert x != null;// 保存被删除节点的元素值final E element = x.item;// 存储被删除节点的前驱和后继节点final LinkedList.Node<E> next = x.next;final LinkedList.Node<E> prev = x.prev;// 如果被删除节点是头节点,则将头指针指向其后继节点if (prev == null) {first = next;} else {// 否则更新被删除节点的前驱节点的 next 属性为其后继节点,并将被删除节点的 prev 属性设为 nullprev.next = next;x.prev = null;}// 如果被删除节点是尾节点,则将尾指针指向其前驱节点if (next == null) {last = prev;} else {// 否则更新被删除节点的后继节点的 prev 属性为其前驱节点,并将被删除节点的 next 属性设为 nullnext.prev = prev;x.next = null;}// 将被删除节点的 item 属性设为 nullx.item = null;// 更新 LinkedList 的元素数量和修改次数size--;modCount++;// 返回被删除节点的元素值return element;
}

3.5 get()方法

/*** 用于获取指定索引处的元素*/
public E get(int index) {// 检查索引的有效性,然后调用另一个私有辅助方法 node 来获取对应索引处的节点checkElementIndex(index);// 返回该节点的元素值return node(index).item;
}/*** 检查指定索引是否在有效范围内*/
private void checkElementIndex(int index) {// 调用 isElementIndex 方法来判断索引是否在有效范围内if (!isElementIndex(index))// 如果不在,则抛出 IndexOutOfBoundsException 异常,异常消息由 outOfBoundsMsg 方法返回throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}/*** 判断指定索引是否在有效范围内*/
private boolean isElementIndex(int index) {// 如果索引大于等于 0 并且小于 size,则返回 true;否则返回 falsereturn index >= 0 && index < size;
}

3.6 set()方法

/*** 用于将指定索引(index)位置的元素替换为新的元素,并返回被替换掉的旧元素*/
public E set(int index, E element) {// 用于将指定索引(index)位置的元素替换为新的元素,并返回被替换掉的旧元素checkElementIndex(index);// 获取指定索引位置上的节点LinkedList.Node<E> x = node(index);// 将指定索引位置上的节点的元素值赋给变量 oldVal,即记录旧元素的值E oldVal = x.item;// 将指定索引位置上的节点的元素值替换为新的元素值 elementx.item = element;// 返回被替换掉的旧元素值return oldVal;
}/*** 检查指定索引是否在有效范围内*/
private void checkElementIndex(int index) {// 调用 isElementIndex 方法来判断索引是否在有效范围内if (!isElementIndex(index))// 如果不在,则抛出 IndexOutOfBoundsException 异常,异常消息由 outOfBoundsMsg 方法返回throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}/*** 判断指定索引是否在有效范围内*/
private boolean isElementIndex(int index) {// 如果索引大于等于 0 并且小于 size,则返回 true;否则返回 falsereturn index >= 0 && index < size;
}

3.7 clear()方法

/*** 用于清空整个链表的内容,并释放相关的内存空间*/
public void clear() {// Clearing all of the links between nodes is "unnecessary", but:// 解释了清除所有节点之间链接的操作虽然“不必要”,但有两个原因:// - helps a generational GC if the discarded nodes inhabit// 帮助分代垃圾回收(generational GC)//   more than one generation// - is sure to free memory even if there is a reachable Iterator// 和确保释放内存即使存在可达的迭代器// 循环体中的变量 x 初始化为头节点 first,然后在每次迭代中更新为下一个节点,直到遍历完整个链表for (LinkedList.Node<E> x = first; x != null; ) {// 用于保存当前节点 x 的后继节点的引用,以防止在清空当前节点之后丢失对后继节点的引用LinkedList.Node<E> next = x.next;// 将当前节点 x 的元素值、前驱节点和后继节点都设置为 null,从而切断当前节点与前后节点的关联x.item = null;x.next = null;x.prev = null;// 将 x 更新为下一个节点,以便进行下一次循环迭代x = next;}// 清空头节点和尾节点的引用,使整个链表为空first = last = null;// 将链表的大小设置为 0,表示链表中不再包含任何元素size = 0;// 更新修改次数计数器,用于在迭代过程中检测并发修改modCount++;
}

3.8 indexOf()方法

/*** 用于查找指定元素在链表中第一次出现的位置(索引)*/
public int indexOf(Object o) {// 初始化索引变量为 0,表示从头节点开始查找int index = 0;// 如果要查找的元素 o 是 null,则执行下面的 for 循环,按顺序遍历链表,查找第一个元素值为 null 的节点if (o == null) {// 变量 x 初始化为头节点 first,然后在每次迭代中更新为下一个节点,直到遍历完整个链表for (LinkedList.Node<E> x = first; x != null; x = x.next) {// 判断当前节点 x 的元素值是否为 null,如果是,则说明找到了要查找的元素,返回当前索引 index 表示该元素在链表中的位置if (x.item == null)return index;// 如果当前节点不是要查找的元素,则将索引变量 index 加 1,继续向下查找index++;}} else {// 如果要查找的元素 o 不是 null,则执行下面的 for 循环,按顺序遍历链表,查找第一个元素值与 o 相等的节点for (LinkedList.Node<E> x = first; x != null; x = x.next) {// 判断当前节点 x 的元素值是否与要查找的元素 o 相等,如果是,则说明找到了要查找的元素,返回当前索引 index 表示该元素在链表中的位置if (o.equals(x.item))return index;// 如果当前节点不是要查找的元素,则将索引变量 index 加 1,继续向下查找index++;}}// 如果整个链表都遍历完了还没有找到要查找的元素,则返回 -1,表示该元素不存在于链表中return -1;
}

四、 总结及实战应用

4.1 LinkedList适用场景

LinkedList在Java中适用于以下场景

  1. 需要频繁进行插入和删除操作:由于LinkedList是基于链表结构实现的,插入和删除操作的时间复杂度为O(1),因此适合在需要频繁执行这些操作的场景下使用。
  2. 需要实现队列(Queue)或双端队列(Deque)功能:LinkedList实现了QueueDeque接口,可以作为队列或双端队列来使用。
  3. 不需要频繁进行随机访问:由于LinkedList对于随机访问的效率较低,如果不需要频繁通过索引来访问元素,而是更多地进行顺序访问或者在头尾进行操作,那么LinkedList比较适合。
  4. 对内存占用没有过高要求:相比于ArrayList,在一些情况下,由于它的存储结构,LinkedList可能会占用更多的内存空间。

需要注意的是,在某些特定的场景下,可能需要根据具体的需求进行性能测试和选择,以确定使用LinkedList是否能够带来性能上的提升。

4.2 LinkedList与ArrayList的比较

LinkedList和ArrayList是Java中两种常见的集合实现类,它们具有一些不同的特点和适用场景。

LinkedList的特点

  • 基于双向链表实现,每个节点都包含指向前一个节点和后一个节点的引用。
  • 高效地支持插入和删除操作,因为只需要改变前后节点的指针指向即可。
  • 在使用迭代器进行遍历时,效率较高。
  • 对于频繁的插入和删除操作,LinkedList通常比ArrayList更加高效。

ArrayList的特点

  • 基于动态数组实现,内部使用数组来存储元素。
  • 支持随机访问,通过索引可以快速访问元素。
  • 在获取元素和遍历操作方面,ArrayList相对更高效。
  • 对于需要频繁随机访问元素的操作,ArrayList通常比LinkedList更加高效。

综上所述,选择LinkedList还是ArrayList取决于具体的使用场景和需求

  • 如果需要频繁进行插入和删除操作,而对于随机访问的需求较少,则选择LinkedList更合适。
  • 如果需要频繁进行随机访问,插入和删除操作相对较少,则选择ArrayList更合适。

需要注意的是,在多线程环境下,LinkedList和ArrayList都不是线程安全的,如果需要在多线程环境下使用,需要进行适当的同步处理或使用线程安全的集合类。

4.3 LinkedList的使用注意事项

在使用 Java 中的 LinkedList 时,有一些需要注意的事项,包括但不限于以下几点:

  1. 插入和删除效率高:LinkedList 在插入和删除操作上有较高的效率,因为它基于双向链表实现。因此,在需要频繁进行插入和删除操作的场景下,可以考虑使用 LinkedList。
  2. 随机访问效率低:相比于 ArrayList,LinkedList 对于随机访问的效率较低,因为要通过指针一个个地找到目标位置。因此,在需要频繁进行随机访问的场景下,最好选择 ArrayList。
  3. 迭代器遍历高效:LinkedList 的迭代器遍历效率较高,可以高效地进行前向和后向遍历操作。
  4. 注意空间开销:由于 LinkedList 中每个节点都需要存储额外的指针信息,因此相比于 ArrayList,它在存储同样数量的元素时会占用更多的内存空间。
  5. 不是线程安全的:LinkedList 不是线程安全的,如果需要在多线程环境中使用,需要进行适当的同步处理或考虑使用线程安全的集合类。
  6. 谨慎使用大数据量:在处理大数据量的情况下,由于 LinkedList 涉及频繁的节点创建和指针操作,可能会导致性能下降,需要谨慎使用。

综上所述,使用 LinkedList 时需要根据具体的场景和需求进行权衡,特别是在涉及到插入、删除、遍历和空间占用等方面需要特别留意。

4.4 实战应用:设计一个基于LinkedList的LRU缓存算法

LRU(Least Recently Used)算法是一种缓存置换策略,它根据数据最近被访问的时间来决定哪些数据会被保留,哪些数据会被淘汰。在 Java 中,可以通过 LinkedList HashMap 来实现 LRU 缓存算法。

具体实现步骤如下

1.定义一个双向链表和一个哈希表,用于存储缓存数据和快速定位数据。

private class CacheNode {private String key;private Object value;private CacheNode prev;private CacheNode next;public CacheNode(String key, Object value) {this.key = key;this.value = value;}
}private Map<String, CacheNode> cacheMap;
private CacheNode head;
private CacheNode tail;
private int capacity;

2.在构造函数中初始化双向链表和哈希表,并设置缓存容量。

public LRUCache(int capacity) {this.capacity = capacity;cacheMap = new HashMap<>(capacity);head = new CacheNode(null, null);tail = new CacheNode(null, null);head.next = tail;tail.prev = head;
}

3.实现 get 方法,每次获取数据时,将数据移到链表头部,并更新哈希表中的位置信息。

public Object get(String key) {CacheNode node = cacheMap.get(key);if (node == null) {return null;}// 将节点移动到链表头部moveToHead(node);return node.value;
}private void moveToHead(CacheNode node) {// 先将节点从原有位置删除removeNode(node);// 将节点插入到链表头部insertNodeAtHead(node);
}private void removeNode(CacheNode node) {node.prev.next = node.next;node.next.prev = node.prev;
}private void insertNodeAtHead(CacheNode node) {node.next = head.next;node.next.prev = node;head.next = node;node.prev = head;
}

4.实现 put 方法,每次存储数据时,如果缓存已满,则淘汰链表尾部的数据,并在哈希表中删除相应的位置信息。

public void put(String key, Object value) {CacheNode node = cacheMap.get(key);if (node != null) {// 如果键已经存在,则更新值,并移到链表头部node.value = value;moveToHead(node);} else {// 如果键不存在,则插入新节点到链表头部,并添加位置信息到哈希表node = new CacheNode(key, value);cacheMap.put(key, node);insertNodeAtHead(node);if (cacheMap.size() > capacity) {// 如果缓存已满,则淘汰链表尾部的节点,并删除相应位置信息CacheNode removedNode = removeNodeAtTail();cacheMap.remove(removedNode.key);}}
}private CacheNode removeNodeAtTail() {CacheNode removedNode = tail.prev;removeNode(removedNode);return removedNode;
}

这样,一个基于 LinkedList 的 LRU 缓存算法就实现了。

可以通过如下代码进行简单的测试

LRUCache cache = new LRUCache(2);
cache.put("A", 1);
cache.put("B", 2);
System.out.println(cache.get("A")); // 输出 1
cache.put("C", 3);
System.out.println(cache.get("B")); // 输出 null

盈若安好,便是晴天

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

SQL server 数据库练习题及答案(练习2)

使用你的名字创建一个数据库 创建表&#xff1a; 数据库中有三张表&#xff0c;分别为student,course,SC&#xff08;即学生表&#xff0c;课程表&#xff0c;选课表&#xff09; 问题&#xff1a; --1.分别查询学生表和学生修课表中的全部数据。--2.查询成绩在70到80分之间…

dhcp的配置

原理 就是服务器&#xff08;路由器或者交换机分配网段&#xff09; 动态分配 接口资源池 全局资源池 静态分配 实验 ar1 ip地址 r1-r3 dhcp en 打开dhcp en 第三步 配置地址池 接口 进入端口 dhcp select interface 设置接口资源池 dhcp server dns-…

5G NR无线蜂窝系统的信道估计器设计

文章目录 DMRS简介DMRS类型DMRS频域密度 信道估计实验仿真实验参数实验实验结论 DMRS简介 DMRS类型 类型A&#xff1a;DMRS位于时隙的第二个或第三个OFDM符号&#xff0c;由14个OFDM符号组成&#xff0c;当数据占据大部分时隙时使用A型映射。 类型B&#xff1a;用在URLLC中&a…

【Mybatis】深入学习MyBatis:概述、主要特性以及配置与映射

&#x1f34e;个人博客&#xff1a;个人主页 &#x1f3c6;个人专栏&#xff1a; Mybatis ⛳️ 功不唐捐&#xff0c;玉汝于成 目录 前言 正文 一、概述 MyBatis简介 主要特性 1. 动态SQL 2.结果映射 3 .插件机制 二、MyBatis配置文件 1.配置文件结构 数据库连…

15 款Python编辑器的优缺点,别再问我“选什么编辑器”

本文介绍了多个 Python IDE&#xff0c;并评价其优缺点。读者可以参考此文列举的 Python IDE 列表&#xff0c;选择适合自己的编辑器。 写 Python 代码最好的方式莫过于使用集成开发环境&#xff08;IDE&#xff09;了。它们不仅能使你的工作更加简单、更具逻辑性&#xff0c;…

Spring Boot整合MyBatis-Plus框架快速上手

最开始&#xff0c;我们要在Java中使用数据库时&#xff0c;需要使用JDBC&#xff0c;创建Connection、ResultSet等&#xff0c;然后我们又对JDBC的操作进行了封装&#xff0c;创建了许多类似于DBUtil等工具类。再慢慢的&#xff0c;出现了一系列持久层的框架&#xff1a;Hiber…

GBase南大通用-GBase 8a资源管理功能试用

环境&#xff1a;centos7.9&#xff1b;GBase 8a V9.5.3.27 资源管理功能简介 GBase南大通用的GBase 8a MPP Cluster 资源管理功能可以对 SELECT 和 DML 等受控 SQL 在运 行过程中使用的 CPU、内存、I/O 和磁盘空间等资源进行合理管控&#xff0c;以达到资 源合理利用&#x…

【toolschain algorithm cpp ros】cpp工厂模式实现--后续填充具体规划算法,控制器版的已填充了算法接入了仿真器

写在前面 现在局势危机&#xff0c;于是想复习一下之前写的设计模式&#xff0c;之前提到&#xff0c;做过一个闭环仿真器&#xff08;借用ros&#xff09;&#xff0c;见https://blog.csdn.net/weixin_46479223/article/details/134864123我的控制器的建立遵循了工厂模式&…

【低照度图像增强系列(2)】Retinex(SSR/MSR/MSRCR)算法详解与代码实现

前言 ☀️ 在低照度场景下进行目标检测任务&#xff0c;常存在图像RGB特征信息少、提取特征困难、目标识别和定位精度低等问题&#xff0c;给检测带来一定的难度。 &#x1f33b;使用图像增强模块对原始图像进行画质提升&#xff0c;恢复各类图像信息&#xff0c;再使用目标检…

C#与php自定义数据流传输

C#与php自定义数据流传输 介绍一、客户端与服务器数据传输流程图客户端发送数据给服务器&#xff1a;服务器返回数据给客户端&#xff1a; 二、自定义数据流C#版本数据流PHP版本数据流 三、数据传输测试1.在Unity中创建一个C#脚本NetWorkManager.cs2.服务器www目录创建StreamTe…

【Linux驱动】驱动框架的进化 | 总线设备驱动模型

&#x1f431;作者&#xff1a;一只大喵咪1201 &#x1f431;专栏&#xff1a;《Linux驱动》 &#x1f525;格言&#xff1a;你只管努力&#xff0c;剩下的交给时间&#xff01; 目录 &#x1f969;驱动框架的进化&#x1f960;分层&#x1f960;面向对象&#x1f960;编程&am…

使用 Jekyll 构建你的网站 - 初入门

文章目录 一、Jekyll介绍二、Jekyll安装和启动2.1 配置Ruby环境1&#xff09;Windows2&#xff09;macOS 2.2 安装 Jekyll2.3 构建Jekyll项目2.4 启动 Jekyll 服务 三、Jekyll常用命令四、目录结构4.1 主要目录4.2 其他的约定目录 五、使用GitLink构建Jekyll博客5.1 生成Jekyll…

同义词替换器降低论文重复率的最新技术解析

大家好&#xff0c;今天来聊聊同义词替换器降低论文重复率的最新技术解析&#xff0c;希望能给大家提供一点参考。 以下是针对论文重复率高的情况&#xff0c;提供一些修改建议和技巧&#xff0c;可以借助此类工具&#xff1a; 标题&#xff1a;同义词替换器降低论文重复率的最…

跟着LearnOpenGL学习11--材质

文章目录 一、材质二、设置材质三、光的属性四、不同的光源颜色 一、材质 在现实世界里&#xff0c;每个物体会对光产生不同的反应。 比如&#xff0c;钢制物体看起来通常会比陶土花瓶更闪闪发光&#xff0c;一个木头箱子也不会与一个钢制箱子反射同样程度的光。 有些物体反…

使用Clion配置Qt开发过程中的很多坑

如果你想使用Clion开发Qt软件 如果你想在Windows上使用Clion开发Qt 如果你还想使用MSVC编译器开发Qt 但是却遇到了各种各种编译报错&#xff0c;那么恭喜你这些坑都有人帮你踩过了 报错一 CMake Error at CMakeLists.txt:25 (find_package):Could not find a package config…

冒泡排序(C语言)

void BubbleSort(int arr[], int len) {int i, j, temp;for (i 0; i < len; i){for (j len - 1; j > i; j--){if (arr[j] > arr[j 1]){temp arr[j];arr[j] arr[j 1];arr[j 1] temp;}}} } 优化&#xff1a; 设置标志位flag&#xff0c;如果发生了交换flag设置…

西南科技大学计算机网络实验二 (IP协议分析与以太网协议分析)

一、实验目的 通过分析由跟踪执行traceroute程序发送和接收捕获得到的IP 数据报,深入研究在IP 数据报中的各种字段,理解IP协议。基于ARP命令和Ethereal进行以太网帧捕获与分析,理解和熟悉ARP协议原理以及以太网帧格式。 二、实验环境 与因特网连接的计算机网络系统;主机操…

ES-mapping

类似数据库中的表结构定义&#xff0c;主要作用如下 定义Index下的字段名( Field Name) 定义字段的类型&#xff0c;比如数值型、字符串型、布尔型等定义倒排索引相关的配置&#xff0c;比如是否索引、记录 position 等 index_options 用于控制倒排索记录的内容&#xff0c;有如…

敏捷开发 - 知识普及

敏捷开发- Scrum 前言 知乎有一篇文章描写Scrum,我觉得比较好:https://zhuanlan.zhihu.com/p/631459977 简单科普下PM和PMO 原文来源:https://zhuanlan.zhihu.com/p/546820914 PM - 项目经理(Project Manager) ​ 需要具备以下能力 ​ 1.号召力 2.影响力 3.交流能力 4.应…

MySQL 导入数据报错MySQL server has gone away

SQL语句太大了 稍微难以测试和验证&#xff0c;但是MySQL使用最大数据包站站点进行服务器和客户端之间的通信。如果语句包含大字段&#xff0c;则可能由于SQL语句的大小&#xff0c;而被中止。 我们可以通过语句查看一下允许的最大包大小&#xff1a;show global variables lik…