当前位置: 首页 > news >正文

链表的 哑结点的本质

哑节点使用详解:为什么不能直接移动dummy?🤔

🎯 问题描述

在链表创建过程中,很多同学会疑惑:既然要用哑节点,为什么需要额外的 current 变量?能不能直接在 dummy 上操作,省去一个变量声明?

// 方法一:使用current变量
function createList(list) {let dummy = new ListNode(0);let current = dummy;  // 为什么需要这个变量?while (list.length) {let val = list.shift();current.next = new ListNode(val);current = current.next;}return dummy.next;
}// 方法二:直接操作dummy
function createList(list) {let dummy = new ListNode(0);// 能不能省去current,直接这样?while (list.length) {let val = list.shift();dummy.next = new ListNode(val);dummy = dummy.next;  // 直接移动dummy}return dummy.next;
}

答案:不可以! 两种方法有本质的不同,第二种方法是错误的。

🧩 深度解析:为什么dummy能"看到"所有current的变化?

关键疑问

你提出了一个非常深刻的问题:

“既然 currentdummy 在后期操作时没有关系,为什么 dummy 又保存了所有 current 的变化?”

这个问题的答案涉及到链表连接的本质对象引用的传递性

🔗 链表连接的本质

关键在于理解:我们不是在 dummy 上直接保存变化,而是通过节点间的 next 指针连接形成链表!

/*** 链表连接的本质演示*/
function demonstrateChaining() {// 创建三个独立的节点let node0 = new ListNode(0);let node1 = new ListNode(1);let node2 = new ListNode(2);console.log('=== 初始状态:三个独立节点 ===');console.log('node0:', node0.val, 'next:', node0.next);console.log('node1:', node1.val, 'next:', node1.next);console.log('node2:', node2.val, 'next:', node2.next);// 通过next指针连接节点node0.next = node1;  // node0指向node1node1.next = node2;  // node1指向node2console.log('\n=== 连接后:形成链表 ===');console.log('从node0开始遍历:');let current = node0;while (current) {console.log('访问节点:', current.val);current = current.next;}console.log('\n=== 关键理解 ===');console.log('node0本身没有"保存"node1和node2的变化');console.log('但通过next指针,node0可以"到达"整个链表');console.log('这就是链表的本质:通过指针连接形成数据结构');
}demonstrateReference();

💡 对象引用的传递性

/*** 详细解释:dummy如何"看到"所有变化*/
function explainChainConnection() {let dummy = new ListNode(0);let current = dummy;console.log('=== 步骤1:初始状态 ===');console.log('dummy和current指向同一个节点[0]');console.log('dummy === current:', dummy === current);  // true// 第一次操作console.log('\n=== 步骤2:第一次添加节点 ===');console.log('执行:current.next = new ListNode(1)');current.next = new ListNode(1);console.log('这个操作的本质:');console.log('- 创建了一个新节点[1]');console.log('- 将current指向的节点[0]的next指向新节点[1]');console.log('- 由于dummy也指向节点[0],所以dummy.next也指向节点[1]');console.log('dummy.next.val:', dummy.next.val);  // 1console.log('\n执行:current = current.next');current = current.next;console.log('这个操作的本质:');console.log('- current变量重新指向节点[1]');console.log('- dummy变量仍然指向节点[0]');console.log('- 但节点[0]的next仍然指向节点[1]');console.log('dummy === current:', dummy === current);  // falseconsole.log('dummy.val:', dummy.val);      // 0console.log('current.val:', current.val); // 1// 第二次操作console.log('\n=== 步骤3:第二次添加节点 ===');console.log('执行:current.next = new ListNode(2)');current.next = new ListNode(2);console.log('这个操作的本质:');console.log('- 创建了一个新节点[2]');console.log('- 将current指向的节点[1]的next指向新节点[2]');console.log('- dummy仍指向节点[0],但可以通过next链到达节点[2]');console.log('dummy.next.next.val:', dummy.next.next.val);  // 2console.log('\n执行:current = current.next');current = current.next;console.log('最终状态:');console.log('dummy指向:', dummy.val);      // 0console.log('current指向:', current.val); // 2console.log('但dummy可以访问整个链表:');let temp = dummy;let result = [];while (temp) {result.push(temp.val);temp = temp.next;}console.log('从dummy遍历:', result.join(' -> '));
}explainChainConnection();

🎯 核心原理:节点连接 vs 变量指向

这里有两个不同的概念,很容易混淆:

1. 变量指向(Variable Pointing)
let dummy = new ListNode(0);    // dummy指向节点A
let current = dummy;            // current也指向节点Acurrent = current.next;         // current重新指向节点B
// 现在:dummy仍指向节点A,current指向节点B
// 这是变量层面的指向变化
2. 节点连接(Node Linking)
let node0 = new ListNode(0);
let node1 = new ListNode(1);node0.next = node1;             // 节点0连接到节点1
// 这是数据结构层面的连接关系
// 无论哪个变量指向node0,都能通过next访问到node1

📊 内存结构详解

让我们用内存图来理解这个过程:

/*** 内存结构变化详解*/
function memoryStructureDemo() {console.log('=== 内存结构变化过程 ===\n');// 初始状态let dummy = new ListNode(0);let current = dummy;console.log('1️⃣ 初始状态:');console.log('内存中有1个节点:');console.log('Node_A: { val: 0, next: null }');console.log('dummy -> Node_A');console.log('current -> Node_A');console.log();// 第一次添加current.next = new ListNode(1);console.log('2️⃣ 执行 current.next = new ListNode(1) 后:');console.log('内存中有2个节点:');console.log('Node_A: { val: 0, next: Node_B }');console.log('Node_B: { val: 1, next: null }');console.log('dummy -> Node_A(仍然指向Node_A)');console.log('current -> Node_A(仍然指向Node_A)');console.log('关键:Node_A的next现在指向Node_B');console.log();current = current.next;console.log('3️⃣ 执行 current = current.next 后:');console.log('内存中仍有2个节点:');console.log('Node_A: { val: 0, next: Node_B }');console.log('Node_B: { val: 1, next: null }');console.log('dummy -> Node_A(没有变化)');console.log('current -> Node_B(现在指向Node_B)');console.log('关键:dummy仍能通过Node_A.next访问到Node_B');console.log();// 第二次添加current.next = new ListNode(2);console.log('4️⃣ 执行 current.next = new ListNode(2) 后:');console.log('内存中有3个节点:');console.log('Node_A: { val: 0, next: Node_B }');console.log('Node_B: { val: 1, next: Node_C }');console.log('Node_C: { val: 2, next: null }');console.log('dummy -> Node_A(仍然指向Node_A)');console.log('current -> Node_B(仍然指向Node_B)');console.log('关键:现在Node_B的next指向Node_C');console.log();current = current.next;console.log('5️⃣ 执行 current = current.next 后:');console.log('内存中仍有3个节点:');console.log('Node_A: { val: 0, next: Node_B }');console.log('Node_B: { val: 1, next: Node_C }');console.log('Node_C: { val: 2, next: null }');console.log('dummy -> Node_A(从未改变)');console.log('current -> Node_C(现在指向Node_C)');console.log();console.log('🎯 最终理解:');console.log('dummy从未"保存"current的变化');console.log('dummy只是始终指向Node_A');console.log('但通过Node_A -> Node_B -> Node_C的连接');console.log('dummy可以访问到整个链表结构');// 验证遍历console.log('\n📋 验证遍历:');let temp = dummy;let path = [];while (temp) {path.push(`Node(${temp.val})`);temp = temp.next;}console.log('从dummy开始遍历路径:', path.join(' -> '));
}memoryStructureDemo();

🔍 关键误区澄清

误区:以为 dummy 在"保存" current 的变化
真相dummy 只是通过链表连接关系访问到了后续节点

/*** 误区澄清演示*/
function clarifyMisconception() {let dummy = new ListNode(0);let current = dummy;// 构建链表current.next = new ListNode(1);current = current.next;current.next = new ListNode(2);current = current.next;console.log('=== 误区澄清 ===');console.log('❌ 错误理解:dummy保存了current的所有变化');console.log('✅ 正确理解:dummy通过链表连接访问到了所有节点');console.log();console.log('证明1:dummy本身从未改变');console.log('dummy.val:', dummy.val);  // 始终是0console.log();console.log('证明2:dummy和current现在指向不同节点');console.log('dummy === current:', dummy === current);  // falseconsole.log('dummy.val:', dummy.val);      // 0console.log('current.val:', current.val); // 2console.log();console.log('证明3:dummy通过next链访问其他节点');console.log('dummy.next.val:', dummy.next.val);           // 1console.log('dummy.next.next.val:', dummy.next.next.val); // 2console.log();console.log('证明4:如果断开连接,dummy就访问不到后续节点');let originalNext = dummy.next;dummy.next = null;  // 断开连接console.log('断开连接后,从dummy遍历:');let temp = dummy;let result = [];while (temp) {result.push(temp.val);temp = temp.next;}console.log('结果:', result.join(' -> '));  // 只有0// 恢复连接dummy.next = originalNext;console.log('恢复连接后,从dummy遍历:');temp = dummy;result = [];while (temp) {result.push(temp.val);temp = temp.next;}console.log('结果:', result.join(' -> '));  // 0 -> 1 -> 2
}clarifyMisconception();

🎯 核心总结

关键理解:

  1. dummy从未"保存"current的变化

    • dummy 始终指向原始哑节点
    • current 在链表上移动,与 dummy 无直接关系
  2. dummy通过链表连接"看到"所有节点

    • 通过 next 指针的连接关系
    • 形成了 dummy -> node1 -> node2 -> ... 的访问路径
  3. 两个不同的概念

    • 变量指向dummycurrent 指向哪个节点
    • 节点连接:节点之间通过 next 形成的连接关系
  4. 为什么需要current变量

    • 需要一个"移动指针"来遍历和构建链表
    • 需要一个"固定指针"来保持对头节点的引用
    • 这两个职责不能由同一个变量承担

💡 形象比喻

想象一下:

  • dummy 就像是火车站的起点标记,始终在原地不动
  • current 就像是正在铺设铁轨的工人,不断向前移动
  • 铁轨就是节点间的 next 连接
  • 虽然工人在移动,但从起点出发仍然可以沿着铁轨到达任何地方

这就是为什么 dummy 能"看到"所有变化的根本原因!

🤔 深度解析:为什么current和dummy操作效果不同?

关键误区澄清

很多同学会有这样的疑惑:

“既然 let current = dummy 是引用赋值,那么操作 current 和操作 dummy 不是一样的吗?”

答案:不一样! 关键在于理解引用赋值引用重新指向的区别。

🔍 引用机制深度分析

/*** 引用赋值 vs 引用重新指向的区别*/
let dummy = new ListNode(0);  // dummy指向对象A
let current = dummy;          // current也指向对象A// 此时:dummy和current指向同一个对象A
console.log(dummy === current);  // true// 情况1:修改对象属性(两者都会受影响)
dummy.val = 999;
console.log(current.val);  // 999,因为指向同一个对象// 情况2:重新指向(只影响被赋值的变量)
current = new ListNode(1);  // current现在指向新对象B
console.log(dummy.val);     // 999,dummy仍指向对象A
console.log(current.val);   // 1,current指向对象B
console.log(dummy === current);  // false,现在指向不同对象

💡 核心区别:修改属性 vs 重新赋值

/*** 演示:为什么操作current和dummy效果不同*/
function demonstrateReference() {let dummy = new ListNode(0);let current = dummy;console.log('=== 初始状态 ===');console.log('dummy指向:', dummy.val);console.log('current指向:', current.val);console.log('是否同一对象:', dummy === current);// 方法1:修改属性(影响同一对象)console.log('\n=== 修改属性 ===');current.val = 888;console.log('修改current.val后:');console.log('dummy.val:', dummy.val);      // 888,受影响console.log('current.val:', current.val); // 888console.log('是否同一对象:', dummy === current); // true// 方法2:重新赋值(改变引用指向)console.log('\n=== 重新赋值 ===');current = new ListNode(999);console.log('重新赋值current后:');console.log('dummy.val:', dummy.val);      // 888,不受影响console.log('current.val:', current.val); // 999console.log('是否同一对象:', dummy === current); // false
}demonstrateReference();

🎯 链表操作中的实际应用

让我们看看在链表创建中,这两种操作的具体效果:

/*** 详细对比:修改属性 vs 重新赋值在链表中的作用*/
function compareOperations() {console.log('=== 正确方法:只重新赋值current ===');let dummy = new ListNode(0);let current = dummy;// 构建链表:0 -> 1 -> 2current.next = new ListNode(1);  // 修改current指向对象的属性current = current.next;          // 重新赋值current(关键!)current.next = new ListNode(2);  // 修改current指向对象的属性current = current.next;          // 重新赋值currentconsole.log('dummy仍指向:', dummy.val);        // 0console.log('current现在指向:', current.val);  // 2console.log('dummy.next.val:', dummy.next.val); // 1console.log('链表完整:', dummy.val, '->', dummy.next.val, '->', dummy.next.next.val);console.log('\n=== 错误方法:重新赋值dummy ===');let dummy2 = new ListNode(0);// 构建链表:0 -> 1 -> 2dummy2.next = new ListNode(1);   // 修改dummy2指向对象的属性dummy2 = dummy2.next;            // 重新赋值dummy2(问题!)dummy2.next = new ListNode(2);   // 修改dummy2指向对象的属性dummy2 = dummy2.next;            // 重新赋值dummy2console.log('dummy2现在指向:', dummy2.val);     // 2console.log('dummy2.next:', dummy2.next);       // nullconsole.log('丢失了对原始哑节点的引用!');
}compareOperations();

📊 内存引用变化图解

正确方法的内存变化:
初始状态:
dummy ──┐├──> [0]
current ┘第一次操作后:
dummy ──> [0] ──> [1]↑current第二次操作后:
dummy ──> [0] ──> [1] ──> [2]↑current
错误方法的内存变化:
初始状态:
dummy ──> [0]第一次操作后:
[0] ──> [1] ←── dummy↑
原始哑节点(引用丢失)第二次操作后:
[0] ──> [1] ──> [2] ←── dummy↑
原始哑节点(引用丢失)

🔧 实际代码验证

/*** 完整验证:证明两种方法的不同结果*/
class ListNode {constructor(val, next = null) {this.val = val;this.next = next;}
}function printChain(head, name) {let result = [];let current = head;let count = 0;while (current && count < 10) {  // 防止无限循环result.push(current.val);current = current.next;count++;}console.log(`${name}:`, result.join(' -> ') || 'null');
}// 正确方法验证
function correctMethod() {let dummy = new ListNode(0);let current = dummy;// 构建链表 1 -> 2 -> 3for (let i = 1; i <= 3; i++) {current.next = new ListNode(i);current = current.next;  // 只移动current}console.log('=== 正确方法结果 ===');console.log('dummy指向的节点值:', dummy.val);console.log('current指向的节点值:', current.val);console.log('dummy === current:', dummy === current);printChain(dummy, 'dummy链表');printChain(dummy.next, 'dummy.next链表');return dummy.next;
}// 错误方法验证
function wrongMethod() {let dummy = new ListNode(0);// 构建链表 1 -> 2 -> 3for (let i = 1; i <= 3; i++) {dummy.next = new ListNode(i);dummy = dummy.next;  // 移动dummy本身}console.log('\n=== 错误方法结果 ===');console.log('dummy指向的节点值:', dummy.val);console.log('dummy.next:', dummy.next);printChain(dummy, 'dummy链表');printChain(dummy.next, 'dummy.next链表');return dummy.next;
}// 执行验证
const result1 = correctMethod();
const result2 = wrongMethod();console.log('\n=== 最终对比 ===');
printChain(result1, '正确方法返回');
printChain(result2, '错误方法返回');

🎯 核心总结

你的理解有一个关键误区:

  1. 相同点currentdummy 初始时确实指向同一个对象
  2. 不同点:当我们执行 current = current.next 时,是在重新赋值current变量,而不是修改对象属性

关键区别:

  • current.next = new ListNode(val) ← 修改对象属性(dummy也会"看到")
  • current = current.next ← 重新赋值变量(只影响current,dummy不变)

如果操作dummy:

  • dummy.next = new ListNode(val) ← 修改对象属性
  • dummy = dummy.next ← 重新赋值变量(丢失原始哑节点引用!)

💡 记忆要点

引用赋值的两种操作:

  1. 修改属性obj.property = value → 影响所有指向该对象的引用
  2. 重新赋值obj = newValue → 只影响当前变量,其他引用不变

在链表操作中:

  • 我们需要修改节点的 next 属性来构建链表
  • 我们需要移动工作指针来遍历,但不能移动哑节点引用
  • 这就是为什么需要 current 变量的根本原因!

记住:变量重新赋值只影响该变量本身,不影响其他指向原对象的变量!

💡 核心原理分析

关键概念:引用 vs 值

在JavaScript中,对象是通过引用传递的。理解这一点是解决问题的关键。

/*** 引用示例演示*/
let obj1 = { value: 1 };
let obj2 = obj1;  // obj2指向同一个对象console.log(obj1 === obj2);  // true,指向同一个对象obj2 = { value: 2 };  // obj2现在指向新对象
console.log(obj1 === obj2);  // false,指向不同对象
console.log(obj1.value);     // 1,原对象未改变

哑节点的设计目的

/*** 哑节点的核心作用** 哑节点(dummy node)是链表操作中的经典模式:* 1. 简化边界条件处理(不需要特判空链表)* 2. 提供稳定的头节点引用* 3. 统一插入操作的逻辑*/

🔍 执行过程对比

方法一:正确做法 ✅

function createListCorrect(list) {let dummy = new ListNode(0);  // dummy始终指向这个哑节点let current = dummy;          // current用来移动// 执行过程分析:// 初始状态:dummy -> [0], current -> [0]while (list.length) {let val = list.shift();current.next = new ListNode(val);current = current.next;  // 只移动current// 关键:dummy始终指向原始哑节点[0]// current在链表上移动:[0] -> [1] -> [2] -> ...}// 最终:dummy仍指向[0],可以返回dummy.nextreturn dummy.next;  // 返回真正的头节点
}

内存布局:

dummy -> [0] -> [1] -> [2] -> [3] -> null↑固定           ↑current最终位置

方法二:错误做法 ❌

function createListWrong(list) {let dummy = new ListNode(0);  // dummy初始指向哑节点// 执行过程分析:// 初始状态:dummy -> [0]while (list.length) {let val = list.shift();dummy.next = new ListNode(val);dummy = dummy.next;  // 致命错误:移动了dummy本身!// 问题:dummy现在指向新节点,丢失了原始哑节点的引用// 第一次:dummy -> [1](丢失了[0]的引用)// 第二次:dummy -> [2](丢失了[1]的引用)// ...}// 最终:dummy指向最后一个节点,dummy.next为nullreturn dummy.next;  // 返回null!
}

内存布局:

[0] -> [1] -> [2] -> [3] -> null↑dummy最终位置
↑原始哑节点(引用丢失)

🧪 实际测试验证

/*** 测试用例验证*/
class ListNode {constructor(val, next = null) {this.val = val;this.next = next;}
}// 辅助函数:链表转数组
function listToArray(head) {const result = [];let current = head;while (current) {result.push(current.val);current = current.next;}return result;
}// 测试数据
const testData = [1, 2, 3, 4, 5];// 测试正确方法
const result1 = createListCorrect([...testData]);
console.log('正确方法结果:', listToArray(result1));
// 输出: [1, 2, 3, 4, 5]// 测试错误方法
const result2 = createListWrong([...testData]);
console.log('错误方法结果:', listToArray(result2));
// 输出: [](空数组,因为head为null)

📊 详细执行步骤

正确方法执行过程

步骤操作dummy指向current指向链表状态
初始创建哑节点[0][0][0]
1插入1[0][1][0]->[1]
2插入2[0][2][0]->[1]->[2]
3插入3[0][3][0]->[1]->[2]->[3]
4插入4[0][4][0]->[1]->[2]->[3]->[4]
5插入5[0][5][0]->[1]->[2]->[3]->[4]->[5]
返回dummy.next[0][5]返回[1]节点

错误方法执行过程

步骤操作dummy指向链表状态问题
初始创建哑节点[0][0]正常
1插入1,移动dummy[1][0]->[1]❌丢失[0]引用
2插入2,移动dummy[2][1]->[2]❌丢失[1]引用
3插入3,移动dummy[3][2]->[3]❌丢失[2]引用
4插入4,移动dummy[4][3]->[4]❌丢失[3]引用
5插入5,移动dummy[5][4]->[5]❌丢失[4]引用
返回dummy.next[5][5]->null❌返回null

🎯 根本原因分析

1. 引用丢失问题

// 问题的本质
let dummy = new ListNode(0);     // dummy指向哑节点A
let head = dummy;                // head也指向哑节点Adummy = dummy.next;              // dummy现在指向别的节点B
// 此时:head仍指向A,dummy指向B
// 如果没有head变量,我们就永远找不到A了!

2. 设计模式违背

哑节点模式的核心原则:

  • 哑节点创建后保持不变
  • 使用工作指针进行遍历
  • 通过哑节点获取真正的头节点
/*** 标准哑节点模式模板*/
function dummyNodePattern() {let dummy = new ListNode(0);  // 1. 创建哑节点(固定不变)let worker = dummy;           // 2. 创建工作指针// 3. 使用工作指针进行操作while (condition) {worker.next = new ListNode(value);worker = worker.next;     // 只移动工作指针}return dummy.next;            // 4. 返回真正的头节点
}

🔧 正确的实现方式

/*** 链表创建 - 标准实现** 核心思想:* 使用哑节点简化链表操作,通过工作指针构建链表** @param {number[]} values - 要创建的值数组* @returns {ListNode} 链表头节点* @time O(n) 遍历一次数组* @space O(n) 创建n个节点*/
function createLinkedList(values) {// 边界检查if (!values || values.length === 0) {return null;}// 1. 创建哑节点(重要:创建后不再移动)const dummy = new ListNode(0);// 2. 创建工作指针(负责遍历和构建)let current = dummy;// 3. 遍历数组,构建链表for (const value of values) {current.next = new ListNode(value);current = current.next;  // 只移动工作指针}// 4. 返回真正的头节点return dummy.next;
}/*** 优化版本:避免修改原数组*/
function createLinkedListOptimized(values) {if (!values || values.length === 0) return null;const dummy = new ListNode(0);let current = dummy;// 使用for...of避免shift()的O(n)复杂度for (const value of values) {current.next = new ListNode(value);current = current.next;}return dummy.next;
}

🚨 常见陷阱和错误

陷阱1:直接移动哑节点

// ❌ 错误做法
function createList(values) {let dummy = new ListNode(0);for (const val of values) {dummy.next = new ListNode(val);dummy = dummy.next;  // 错误:移动了哑节点}return dummy.next;  // 返回null
}// ✅ 正确做法
function createList(values) {const dummy = new ListNode(0);  // 使用const强调不变性let current = dummy;for (const val of values) {current.next = new ListNode(val);current = current.next;  // 只移动工作指针}return dummy.next;
}

陷阱2:混淆引用和值

// 理解引用的重要性
let a = new ListNode(1);
let b = a;        // b和a指向同一个对象a.val = 999;      // 修改对象属性
console.log(b.val);  // 999,因为是同一个对象a = new ListNode(2);  // a指向新对象
console.log(b.val);   // 999,b仍指向原对象

陷阱3:忘记边界检查

// ❌ 没有边界检查
function createList(values) {const dummy = new ListNode(0);let current = dummy;for (const val of values) {  // 如果values为null会报错current.next = new ListNode(val);current = current.next;}return dummy.next;
}// ✅ 完整的边界检查
function createList(values) {if (!values || values.length === 0) {return null;}const dummy = new ListNode(0);let current = dummy;for (const val of values) {current.next = new ListNode(val);current = current.next;}return dummy.next;
}

💭 思维要点总结

🎯 核心概念

  1. 哑节点的作用:提供稳定的头节点引用,简化边界处理
  2. 引用vs值:对象通过引用传递,移动引用会丢失原对象
  3. 工作指针模式:用额外指针进行遍历,保持关键引用不变

🔧 实践技巧

  1. 哑节点创建后用const:强调其不变性
  2. 工作指针负责移动:承担遍历和构建的职责
  3. 最后返回dummy.next:获取真正的头节点

⚠️ 常见错误

  1. 直接移动哑节点:丢失头节点引用
  2. 混淆引用概念:不理解对象引用的本质
  3. 忽略边界条件:没有处理空数组等情况

🌟 设计思想

这个问题体现了算法设计中的重要思想:

  • 职责分离:不同变量承担不同职责
  • 不变性原则:关键引用保持不变
  • 模式复用:哑节点是链表操作的通用模式

文章转载自:
http://atheistic.wjrtg.cn
http://betcha.wjrtg.cn
http://airward.wjrtg.cn
http://artillerist.wjrtg.cn
http://anlage.wjrtg.cn
http://anginal.wjrtg.cn
http://brussels.wjrtg.cn
http://bivalve.wjrtg.cn
http://bowlegged.wjrtg.cn
http://campground.wjrtg.cn
http://amplifier.wjrtg.cn
http://allegedly.wjrtg.cn
http://bleach.wjrtg.cn
http://antigenicity.wjrtg.cn
http://brachycranial.wjrtg.cn
http://apolune.wjrtg.cn
http://chastity.wjrtg.cn
http://bardia.wjrtg.cn
http://capoid.wjrtg.cn
http://andradite.wjrtg.cn
http://bangle.wjrtg.cn
http://alternant.wjrtg.cn
http://capriccioso.wjrtg.cn
http://bodyshell.wjrtg.cn
http://boz.wjrtg.cn
http://antheap.wjrtg.cn
http://blackpoll.wjrtg.cn
http://allowably.wjrtg.cn
http://ascribe.wjrtg.cn
http://chemotropic.wjrtg.cn
http://www.dtcms.com/a/281695.html

相关文章:

  • Python 程序设计讲义(1):PyCharm 安装教程
  • WebView JSBridge 无响应问题排查实录 全流程定位桥接调用失效
  • 深度学习·目标检测和语义分割基础
  • 77、【OS】【Nuttx】【启动】caller-saved 和 callee-saved 示例:栈指针和帧指针(上)
  • Qt图形视图框架5-状态机框架
  • Springboot儿童认知图文辅助系统6yhkv(程序+源码+数据库+调试部署+开发环境)带论文文档1万字以上,文末可获取,系统界面在最后面。
  • 再见吧,Windows自带记事本,这个轻量级文本编辑器太香了
  • 基于mybatis的基础操作的思路
  • C++-linux系统编程 8.进程(二)exec函数族详解
  • 终端安全管理系统为什么需要使用,企业需要的桌面管理软件
  • X 射线探伤证考试核心:辐射安全基础知识点梳理
  • golang二级缓存示例
  • HC165并转串
  • js分支语句和循环语句
  • 如何写一份有效的技术简历?
  • vscode输出中文乱码问题的解决
  • QTableView鼠标双击先触发单击信号
  • Vue 常用的 ESLint 规则集
  • resources为什么是类的根目录
  • Linux 基本操作与服务器部署
  • 【高等数学】第三章 微分中值定理与导数的应用——第一节 不定积分的概念与性质
  • Android 图片压缩
  • 21.映射字典的值
  • 【强化学习】Reinforcement Learning基础概述
  • 如何进行 Docker 数据目录迁移
  • 三轴云台之深度学习算法篇
  • vscode配置运行完整C代码项目
  • QGIS新手教程9:字段计算器进阶用法与批量处理技巧
  • onecode 3.0 微内核引擎 基础注解驱动的速查手册(服务治理及通讯)
  • Altium Designer(AD)25软件下载及安装教程(7.9)