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

链表-哨兵节点链表【node5】

基本概念

哨兵节点(Sentinel Node)是链表中的一个特殊节点,它不存储实际数据,仅用于简化某些边界条件的处理。哨兵节点通常位于链表的头部或尾部,有时也可以同时存在于头部和尾部。

哨兵节点的引入主要是为了解决在链表操作中处理空链表和非空链表之间的差异,使代码更加简洁和一致。

作用

哨兵节点主要解决以下问题:

  • 统一空链表和非空链表的处理逻辑:有了哨兵节点,即使链表为空,也始终存在一个节点(哨兵节点),这样就不需要为空链表编写特殊的处理代码。
  • 简化边界条件:在头插、尾插、头删等操作中,不需要再单独判断链表是否为空。
  • 统一链表遍历:有了哨兵节点,遍历可以从哨兵节点的下一个节点开始,无需特殊处理。
  • 减少代码分支:减少if-else条件判断,使代码更加简洁、清晰。

带哨兵节点的链表实现

单链表中的哨兵节点

在单链表中,哨兵节点通常位于链表的头部,被称为"头哨兵"。链表的头指针指向这个哨兵节点,而不是指向第一个有效数据节点。

package lianbiao;import java.util.ArrayList;
import java.util.List;/*** @Author Stringzhua* @Date 2025/10/23 17:10* description:单链表中的哨兵节点*/
public class SentinelSinglyLinkedList {// 链表节点内部类static class Node {Integer data;  // 数据域Node next;     // 指向下一个节点的引用// 无参构造(用于创建哨兵节点,data为null)public Node() {this.data = null;this.next = null;}// 有参构造(用于创建实际数据节点)public Node(Integer data) {this.data = data;this.next = null;}}private Node sentinel;  // 哨兵节点(固定存在,不存储实际数据)// 单哨兵单向链表构造方法(初始化哨兵节点)public SentinelSinglyLinkedList() {this.sentinel = new Node();  // 哨兵节点data默认null,next默认nullthis.sentinel.next = null;}/*** 检查链表是否为空(实际节点为空)* @return 空返回true,非空返回false*/public boolean isEmpty() {return sentinel.next == null;}/*** 在链表头部插入节点(哨兵节点之后)* @param data 插入的节点数据*/public void prepend(Integer data) {Node newNode = new Node(data);// 新节点指向哨兵节点的原后继newNode.next = sentinel.next;// 哨兵节点指向新节点,完成头插sentinel.next = newNode;}/*** 在链表尾部插入节点* @param data 插入的节点数据*/public void append(Integer data) {Node newNode = new Node(data);Node current = sentinel;// 遍历到最后一个实际节点(current.next为null时停止)while (current.next != null) {current = current.next;}// 最后一个节点指向新节点current.next = newNode;}/*** 删除第一个实际节点(哨兵节点的后继)* @return 删除成功返回true,空链表返回false*/public boolean deleteFirst() {if (isEmpty()) {return false;}// 哨兵节点跳过第一个实际节点,直接指向其下一个sentinel.next = sentinel.next.next;return true;}/*** 删除指定数据的实际节点* @param data 要删除的节点数据* @return 删除成功返回true,未找到/空链表返回false*/public boolean delete(Integer data) {if (isEmpty()) {return false;}Node prev = sentinel;       // 前驱节点(从哨兵开始)Node current = sentinel.next;  // 当前节点(第一个实际节点)while (current != null) {// 匹配数据时,前驱节点跳过当前节点if (current.data.equals(data)) {prev.next = current.next;return true;}prev = current;current = current.next;}return false;  // 遍历结束未找到目标节点}/*** 遍历链表,返回所有实际节点的数据* @return 存储节点数据的List集合*/public List<Integer> traverse() {List<Integer> result = new ArrayList<>();Node current = sentinel.next;  // 从第一个实际节点开始遍历while (current != null) {result.add(current.data);current = current.next;}return result;}public static void main(String[] args) {// 1. 创建单哨兵单向链表实例SentinelSinglyLinkedList sll = new SentinelSinglyLinkedList();// 2. 初始链表为空检查System.out.println("初始链表是否为空: " + sll.isEmpty());  // 输出: true// 3. 头部插入元素(10, 20, 30)sll.prepend(30);sll.prepend(20);sll.prepend(10);System.out.println("头部插入10, 20, 30后链表内容: " + sll.traverse());  // 输出: [10, 20, 30]// 4. 尾部插入元素(40, 50)sll.append(40);sll.append(50);System.out.println("尾部插入40, 50后链表内容: " + sll.traverse());  // 输出: [10, 20, 30, 40, 50]// 5. 删除第一个节点sll.deleteFirst();System.out.println("删除第一个节点后链表内容: " + sll.traverse());  // 输出: [20, 30, 40, 50]// 6. 删除指定值节点(存在:30)sll.delete(30);System.out.println("删除值为30的节点后链表内容: " + sll.traverse());  // 输出: [20, 40, 50]// 7. 删除指定值节点(不存在:60)boolean success = sll.delete(60);System.out.println("尝试删除值为60的节点是否成功: " + success);  // 输出: false// 8. 再次检查链表是否为空System.out.println("链表是否为空: " + sll.isEmpty());  // 输出: false// 9. 清空链表(删除所有实际节点)sll.deleteFirst();sll.deleteFirst();sll.deleteFirst();System.out.println("清空链表后链表内容: " + sll.traverse());  // 输出: []System.out.println("清空后链表是否为空: " + sll.isEmpty());  // 输出: true// 10. 对空链表执行删除第一个节点操作success = sll.deleteFirst();System.out.println("对空链表删除第一个节点是否成功: " + success);  // 输出: false}
}

双向链表中的哨兵节点

在双向链表中,同样可以使用哨兵节点简化操作。双向链表的哨兵节点既可以是头哨兵,也可以是尾哨兵,或者两者都有。

package lianbiao;import java.util.ArrayList;
import java.util.List;/*** @Author Stringzhua* @Date 2025/10/23 17:30* description:双向链表中的哨兵节点*/
public class SentinelDoublyLinkedList {// 双向链表节点内部类static class DoublyNode {Integer data;  // 数据域,支持nullDoublyNode prev;  // 前驱节点引用DoublyNode next;  // 后继节点引用// 构造方法public DoublyNode() {this.data = null;this.prev = null;this.next = null;}public DoublyNode(Integer data) {this.data = data;this.prev = null;this.next = null;}}private DoublyNode sentinel;  // 哨兵节点(核心:固定存在,简化边界处理)// 构造方法:初始化哨兵节点形成自闭环public SentinelDoublyLinkedList() {this.sentinel = new DoublyNode();// 初始状态:哨兵的前驱和后继都指向自身,形成空链表闭环this.sentinel.prev = this.sentinel;this.sentinel.next = this.sentinel;}/*** 检查链表是否为空* 空链表判定:哨兵的后继指向自身(无实际节点)*/public boolean isEmpty() {return sentinel.next == sentinel;}/*** 在链表头部插入节点(哨兵节点之后)* @param data 插入的节点数据*/public void prepend(Integer data) {DoublyNode newNode = new DoublyNode(data);// 1. 新节点前驱指向哨兵,后继指向哨兵原后继newNode.prev = sentinel;newNode.next = sentinel.next;// 2. 哨兵原后继的前驱指向新节点sentinel.next.prev = newNode;// 3. 哨兵的后继指向新节点,完成头插sentinel.next = newNode;}/*** 在链表尾部插入节点(哨兵节点之前)* @param data 插入的节点数据*/public void append(Integer data) {DoublyNode newNode = new DoublyNode(data);// 1. 新节点后继指向哨兵,前驱指向哨兵原前驱newNode.next = sentinel;newNode.prev = sentinel.prev;// 2. 哨兵原前驱的后继指向新节点sentinel.prev.next = newNode;// 3. 哨兵的前驱指向新节点,完成尾插sentinel.prev = newNode;}/*** 删除第一个实际节点(哨兵的后继节点)* @return 删除成功返回true,空链表返回false*/public boolean deleteFirst() {if (isEmpty()) {return false;}DoublyNode nodeToDelete = sentinel.next;  // 待删除节点// 1. 哨兵的后继指向待删除节点的后继sentinel.next = nodeToDelete.next;// 2. 待删除节点的后继的前驱指向哨兵nodeToDelete.next.prev = sentinel;return true;}/*** 删除最后一个实际节点(哨兵的前驱节点)* @return 删除成功返回true,空链表返回false*/public boolean deleteLast() {if (isEmpty()) {return false;}DoublyNode nodeToDelete = sentinel.prev;  // 待删除节点// 1. 哨兵的前驱指向待删除节点的前驱sentinel.prev = nodeToDelete.prev;// 2. 待删除节点的前驱的后继指向哨兵nodeToDelete.prev.next = sentinel;return true;}/*** 删除指定数据的节点* @param data 要删除的节点数据* @return 删除成功返回true,未找到/空链表返回false*/public boolean delete(Integer data) {if (isEmpty()) {return false;}DoublyNode current = sentinel.next;  // 从第一个实际节点开始遍历// 遍历终止条件:回到哨兵节点(闭环结束)while (current != sentinel) {if (current.data.equals(data)) {// 1. 当前节点的前驱的后继指向当前节点的后继current.prev.next = current.next;// 2. 当前节点的后继的前驱指向当前节点的前驱current.next.prev = current.prev;return true;}current = current.next;}return false;  // 未找到目标节点}/*** 正向遍历链表(从哨兵后继到哨兵)* @return 存储节点数据的List*/public List<Integer> traverse() {List<Integer> result = new ArrayList<>();DoublyNode current = sentinel.next;while (current != sentinel) {result.add(current.data);current = current.next;}return result;}/*** 反向遍历链表(从哨兵前驱到哨兵)* @return 存储节点数据的List*/public List<Integer> reverseTraverse() {List<Integer> result = new ArrayList<>();DoublyNode current = sentinel.prev;while (current != sentinel) {result.add(current.data);current = current.prev;}return result;}public static void main(String[] args) {// 1. 创建带哨兵的双向链表实例SentinelDoublyLinkedList dll = new SentinelDoublyLinkedList();// 2. 初始链表为空检查System.out.println("初始链表是否为空: " + dll.isEmpty());  // 输出: true// 3. 头部插入元素(10, 20, 30)dll.prepend(30);dll.prepend(20);dll.prepend(10);System.out.println("头部插入10, 20, 30后链表内容(正向): " + dll.traverse());  // 输出: [10, 20, 30]System.out.println("头部插入10, 20, 30后链表内容(反向): " + dll.reverseTraverse());  // 输出: [30, 20, 10]// 4. 尾部插入元素(40, 50)dll.append(40);dll.append(50);System.out.println("尾部插入40, 50后链表内容(正向): " + dll.traverse());  // 输出: [10, 20, 30, 40, 50]System.out.println("尾部插入40, 50后链表内容(反向): " + dll.reverseTraverse());  // 输出: [50, 40, 30, 20, 10]// 5. 删除第一个节点dll.deleteFirst();System.out.println("删除第一个节点后链表内容(正向): " + dll.traverse());  // 输出: [20, 30, 40, 50]System.out.println("删除第一个节点后链表内容(反向): " + dll.reverseTraverse());  // 输出: [50, 40, 30, 20]// 6. 删除最后一个节点dll.deleteLast();System.out.println("删除最后一个节点后链表内容(正向): " + dll.traverse());  // 输出: [20, 30, 40]System.out.println("删除最后一个节点后链表内容(反向): " + dll.reverseTraverse());  // 输出: [40, 30, 20]// 7. 删除指定值节点(存在:30)dll.delete(30);System.out.println("删除值为30的节点后链表内容(正向): " + dll.traverse());  // 输出: [20, 40]System.out.println("删除值为30的节点后链表内容(反向): " + dll.reverseTraverse());  // 输出: [40, 20]// 8. 删除指定值节点(不存在:60)boolean success = dll.delete(60);System.out.println("尝试删除值为60的节点是否成功: " + success);  // 输出: false// 9. 检查链表是否为空System.out.println("链表是否为空: " + dll.isEmpty());  // 输出: false// 10. 清空链表(删除所有节点)dll.deleteFirst();dll.deleteFirst();System.out.println("清空链表后链表内容(正向): " + dll.traverse());  // 输出: []System.out.println("清空后链表是否为空: " + dll.isEmpty());  // 输出: true// 11. 对空链表执行删除操作success = dll.deleteFirst();System.out.println("对空链表删除第一个节点是否成功: " + success);  // 输出: falsesuccess = dll.deleteLast();System.out.println("对空链表删除最后一个节点是否成功: " + success);  // 输出: false}
}

循环链表中的哨兵节点

在循环链表中,哨兵节点可以作为链表的起点和终点,使链表形成一个真正的循环,同时简化边界条件处理。

package lianbiao;import java.util.ArrayList;
import java.util.List;/*** @Author Stringzhua* @Date 2025/10/23 17:59* description:循环链表中的哨兵节点*/
public class SentinelCircularLinkedList {// 循环链表节点内部类static class Node {Integer data;  // 数据域,支持nullNode next;     // 指向下一个节点的引用// 无参构造(用于哨兵节点)public Node() {this.data = null;this.next = null;}// 有参构造(用于实际数据节点)public Node(Integer data) {this.data = data;this.next = null;}}private Node sentinel;  // 哨兵节点(核心:固定存在,形成循环)// 构造方法:初始化哨兵节点形成自闭环public SentinelCircularLinkedList() {this.sentinel = new Node();this.sentinel.next = this.sentinel;  // 哨兵节点的next指向自身,形成空循环}/*** 检查链表是否为空* 空链表判定:哨兵节点的next指向自身(无实际节点)*/public boolean isEmpty() {return sentinel.next == sentinel;}/*** 在链表头部插入节点(哨兵节点之后)* @param data 插入的节点数据*/public void prepend(Integer data) {Node newNode = new Node(data);// 1. 新节点指向哨兵的原后继节点newNode.next = sentinel.next;// 2. 哨兵节点指向新节点,完成头插sentinel.next = newNode;}/*** 在链表尾部插入节点(哨兵节点之前)* @param data 插入的节点数据*/public void append(Integer data) {Node newNode = new Node(data);Node current = sentinel;// 遍历到最后一个实际节点(当前节点的next为哨兵时停止)while (current.next != sentinel) {current = current.next;}// 1. 最后一个节点指向新节点current.next = newNode;// 2. 新节点指向哨兵,维持循环特性newNode.next = sentinel;}/*** 删除第一个实际节点(哨兵的后继节点)* @return 删除成功返回true,空链表返回false*/public boolean deleteFirst() {if (isEmpty()) {return false;}// 哨兵节点跳过第一个实际节点,直接指向其下一个节点sentinel.next = sentinel.next.next;return true;}/*** 删除指定数据的节点* @param data 要删除的节点数据* @return 删除成功返回true,未找到/空链表返回false*/public boolean delete(Integer data) {if (isEmpty()) {return false;}Node prev = sentinel;       // 前驱节点(从哨兵开始)Node current = sentinel.next;  // 当前节点(第一个实际节点)// 遍历终止条件:回到哨兵节点(循环结束)while (current != sentinel) {if (current.data.equals(data)) {// 前驱节点跳过当前节点,完成删除prev.next = current.next;return true;}prev = current;current = current.next;}return false;  // 未找到目标节点}/*** 遍历链表(从哨兵后继到哨兵)* @return 存储节点数据的List集合*/public List<Integer> traverse() {List<Integer> result = new ArrayList<>();Node current = sentinel.next;// 遍历至回到哨兵节点为止while (current != sentinel) {result.add(current.data);current = current.next;}return result;}// 主方法:测试带哨兵的循环单链表功能public static void main(String[] args) {SentinelCircularLinkedList scl = new SentinelCircularLinkedList();// 测试空链表System.out.println("初始链表是否为空: " + scl.isEmpty());  // true// 头部插入元素scl.prepend(30);scl.prepend(20);scl.prepend(10);System.out.println("头部插入10,20,30后: " + scl.traverse());  // [10, 20, 30]// 尾部插入元素scl.append(40);scl.append(50);System.out.println("尾部插入40,50后: " + scl.traverse());  // [10, 20, 30, 40, 50]// 删除第一个节点scl.deleteFirst();System.out.println("删除第一个节点后: " + scl.traverse());  // [20, 30, 40, 50]// 删除指定节点(存在)scl.delete(30);System.out.println("删除30后: " + scl.traverse());  // [20, 40, 50]// 删除指定节点(不存在)boolean success = scl.delete(60);System.out.println("删除60是否成功: " + success);  // false// 清空链表scl.deleteFirst();scl.deleteFirst();scl.deleteFirst();System.out.println("清空后是否为空: " + scl.isEmpty());  // trueSystem.out.println("清空后遍历: " + scl.traverse());  // []// 空链表删除操作success = scl.deleteFirst();System.out.println("空链表删除第一个节点: " + success);  // false}
}

带哨兵节点与不带哨兵节点的链表比较

不带哨兵节点的链表

package lianbiao;// 不带哨兵节点的链表
// 1. 先定义链表节点类
class Node {Integer data;  // 数据域,支持nullNode next;     // 指向下一个节点的引用// 节点构造方法public Node(Integer data) {this.data = data;this.next = null;}
}// 2. 定义单链表类(包含prepend和delete_head方法)
class SinglyLinkedList {private Node head;  // 链表头节点// 链表构造方法(初始化空链表)public SinglyLinkedList() {this.head = null;}/*** 头插法:在链表头部插入节点(处理空链表特殊情况)* @param data 插入的节点数据*/public void prepend(Integer data) {Node newNode = new Node(data);// 特殊处理:空链表(head为null)if (this.head == null) {this.head = newNode;} else {// 非空链表:新节点指向原头节点,再更新头节点newNode.next = this.head;this.head = newNode;}}/*** 删除头节点(处理空链表特殊情况)* @return 删除成功返回true,空链表返回false*/public boolean deleteHead() {// 特殊处理:空链表(head为null)if (this.head == null) {return false;}// 非空链表:头节点指向自身的next(单节点时next为null,自动处理)this.head = this.head.next;return true;}// (可选)辅助方法:打印链表,用于验证功能public void printList() {Node current = this.head;while (current != null) {System.out.print(current.data + " -> ");current = current.next;}System.out.println("null");}public static void main(String[] args) {SinglyLinkedList list = new SinglyLinkedList();// 测试prepend:空链表→插入1→插入2→插入3System.out.println("测试prepend:");list.prepend(1);  // 空链表插入,head=1list.printList(); // 输出:1 -> nulllist.prepend(2);  // 非空插入,head=2→1list.printList(); // 输出:2 -> 1 -> nulllist.prepend(3);  // 非空插入,head=3→2→1list.printList(); // 输出:3 -> 2 -> 1 -> null// 测试deleteHead:删除3→删除2→删除1→删除空链表System.out.println("\n测试deleteHead:");System.out.println("删除头节点是否成功:" + list.deleteHead()); // true,head=2→1list.printList(); // 输出:2 -> 1 -> nullSystem.out.println("删除头节点是否成功:" + list.deleteHead()); // true,head=1list.printList(); // 输出:1 -> nullSystem.out.println("删除头节点是否成功:" + list.deleteHead()); // true,head=nulllist.printList(); // 输出:nullSystem.out.println("删除头节点是否成功:" + list.deleteHead()); // false(空链表)}
}

带哨兵节点的链表

// 1. 哨兵链表的节点类(数据域支持null)
class Node {Integer data;  // data,支持nullNode next;     // 指向下一个节点的引用// 节点构造方法(用于实际数据节点)public Node(Integer data) {this.data = data;this.next = null;}// 无参构造方法(用于哨兵节点,data默认null)public Node() {this.data = null;this.next = null;}
}// 2. 带哨兵节点的链表类(核心:通过哨兵统一处理逻辑)
class SentinelLinkedList {private Node sentinel;  // 哨兵节点(固定存在,不存实际数据)// 链表构造方法:初始化哨兵节点,形成自闭环(空链表状态)public SentinelLinkedList() {this.sentinel = new Node();this.sentinel.next = this.sentinel;  // 哨兵next指向自身,统一空/非空逻辑}/*** 头插法:统一处理空/非空链表,无需特殊判断* @param data 插入的节点数据*/public void prepend(Integer data) {Node newNode = new Node(data);// 1. 新节点指向哨兵的原后继(空链表时指向哨兵,非空时指向原头节点)newNode.next = this.sentinel.next;// 2. 哨兵指向新节点,完成头插(空/非空逻辑完全统一)this.sentinel.next = newNode;}/*** 判断链表是否为空(基于哨兵节点)* @return 空返回true,非空返回false*/public boolean isEmpty() {// 空链表标志:哨兵next指向自身(无实际节点)return this.sentinel.next == this.sentinel;}/*** 删除头节点:仅需检查空链表,无需处理单节点特殊情况* @return 删除成功返回true,空链表返回false*/public boolean deleteHead() {// 仅需判断是否为空,非空时直接跳过原头节点if (this.isEmpty()) {return false;}// 哨兵next指向原头节点的后继(单节点时指向哨兵,自动处理)this.sentinel.next = this.sentinel.next.next;return true;}// (辅助方法)遍历链表,用于验证功能public void printList() {Node current = this.sentinel.next;while (current != this.sentinel) {  // 遍历至哨兵节点停止System.out.print(current.data + " -> ");current = current.next;}System.out.println("sentinel");  // 打印哨兵,明确链表边界}public static void main(String[] args) {SentinelLinkedList list = new SentinelLinkedList();// 1. 测试prepend(空链表→插入1→插入2→插入3)System.out.println("=== 测试prepend ===");System.out.print("空链表初始状态:");list.printList();  // 输出:sentinel(无实际节点)list.prepend(1);System.out.print("插入1后:");list.printList();  // 输出:1 -> sentinellist.prepend(2);System.out.print("插入2后:");list.printList();  // 输出:2 -> 1 -> sentinellist.prepend(3);System.out.print("插入3后:");list.printList();  // 输出:3 -> 2 -> 1 -> sentinel// 2. 测试deleteHead(删除3→删除2→删除1→删除空链表)System.out.println("\n=== 测试deleteHead ===");boolean success = list.deleteHead();System.out.print("删除头节点(3):" + (success ? "成功," : "失败,") + "链表状态:");list.printList();  // 输出:2 -> 1 -> sentinelsuccess = list.deleteHead();System.out.print("删除头节点(2):" + (success ? "成功," : "失败,") + "链表状态:");list.printList();  // 输出:1 -> sentinelsuccess = list.deleteHead();System.out.print("删除头节点(1):" + (success ? "成功," : "失败,") + "链表状态:");list.printList();  // 输出:sentinel(空链表)success = list.deleteHead();System.out.print("删除空链表头节点:" + (success ? "成功," : "失败,") + "链表状态:");list.printList();  // 输出:sentinel}
}

优缺点

带哨兵节点的优点:

  • 代码更加简洁,减少了条件判断
  • 统一了空链表和非空链表的处理逻辑
  • 避免了处理边界条件的繁琐代码
  • 在某些情况下,可以提高代码的执行效率
  • 降低了代码出错的可能性

带哨兵节点的缺点:

  • 额外消耗一个节点的空间
  • 对于简单的链表操作,引入哨兵可能显得过于复杂
  • 对于不熟悉哨兵模式的开发者,可能增加学习成本

注意:

:::info
哨兵节点并不能减少时间复杂度,可以减少一些常数因子,并不是提高速度用的,而是为了简化实现代码,属于编程技巧。虽然它会引入额外的空间开销,但在大多数情况下,这种开销是微不足道的,而其带来的代码简化和降低错误率的益处则更为显著。

:::

http://www.dtcms.com/a/529884.html

相关文章:

  • 中国国家住房和城乡建设部网站首页做网站的商家怎么后去流量费
  • 【Transformer入门到实战】神经网络基础知识和常见激活函数详解
  • 中国建设人才服务信息网是不是正规网站国家药品监督管理局
  • 中药饮片批发市场如何通过创新提升行业竞争力?
  • 宁波网站建设网页设计软件开发和网站开发难度
  • Java List 集合
  • 前缀和算法:高效解决区间和问题
  • 网站设计 价格湖南省建三公司官网
  • 阳江网络公司湖南seo推广方法
  • 丹东网站制作湖南人文科技学院简介
  • pandas转换为日期及期间切片
  • lol视频网站模板wordpress小说站模版
  • 免费申请账号网站卢松松网站
  • 站长统计幸福宝2022年排行榜网站优化过度被k
  • 看英语做游戏的网站长沙微网站
  • 整站优化 快速排名苏州园区人力资源中心
  • LeetCode算法日记 - Day 84: 乘积为正数的最长子数组长度
  • s001网站建设设计个人网站建设实训目的
  • 高端大气的广告公司名字seo关键词优化公司
  • pc网站转换成wapdw做网站环境配置
  • 江门网站建设方案外包做暖暖视频网站
  • 摄影行业网站论坛wordpress还是
  • 软文推广平台推荐:垂直领域精准触达,效果提升新路径
  • 数据库MySQL基础
  • 办网站租服务器大气网站源码
  • ps做图网站做loge的网站
  • wordpress主题样式优化软件
  • 公司怎么建网站做推广做电商网站的公司
  • 怎么用dw做带登陆的网站网站 建设ppt
  • 怎么做网站排版企云网站建设