数据结构与算法-跳表详解

news2024/11/27 16:40:52

我们知道如果一个数组是有序的,查询的时候可以使用二分法进行查询,时间复杂度可以降到 O(logn) ,但如果链表是有序的,我们仍然是从前往后一个个查找,这样显然很慢,这个时候我们可以使用跳表(Skip list),跳表就是多层链表,每一层链表都是有序的,最下面一层是原始链表,包含所有数据,从下往上节点个数逐渐减少,如下图所示。

跳表的特性:

  • 一个跳表有若干层链表组成;
  • 每一层链表都是有序的;​​​​​​​
  • 跳表最下面一层的链表包含所有数据;​​​​​​​
  • 如果一个元素出现在某一次层,那么该层下面的所有层都必须包含该元素;​​​​​​​
  • 上一层的元素指向下层的元素必须是相同的;​​​​​​​
  • 头指针 head 指向最上面一层的第一个元素;

跳表节点的插入

如果是单链表只需要找出待插入节点的前一个节点即可,但在跳表中不光要插入到原始链表中,在他上面的某些层也有可能需要插入,实现方式有随机性和确定性两种选择,其中随机性一般比较常见。比如要在跳表中插入一个元素,可以随机生成一个数字 level ,从 level 层往下每层都要插入。所以跳表中节点不光有 next 属性,还有 down 属性,他指向下一个节点的引用,先来看下跳表的节点类。

 // 跳表的节点类。
 public class SkipListNode {
     // 跳表节点的值,在实际应用中节点类可以加个泛型,这里为了方便介绍,直接使用 int 类型。
     public int val;
     public SkipListNode next;// 指向后面一个节点。
     public SkipListNode down;// 指向下面一层的相同节点。
 
     public SkipListNode(int val, SkipListNode next) {
         this.val = val;
         this.next = next;
    }
 }

在跳表中我们还需要定义一个最大值 MAX_LEVEL ,就是跳表的最大层级数不能超过这个值。我们再来看下索引层级的随机函数,他主要用于在插入节点的时候从第几层开始插入,他是随机的,越往上机率越小,这也符合跳表的特性,越往上节点越少,最大值不能超过 MAX_LEVEL  。

 // 索引层级随机函数。
 private int randLevel() {
     int level = 1;// 1 的概率是0.5,2的概率是0.25,3的概率是0.125,4的概率是0.0625,……
     // Math.random()每次会生成一个 0 到 1 之间的随机数
     while (Math.random() < 0.5f && level < MAX_LEVEL)
         level++;
     return level;
 }

在跳表中每一层都有一个头节点,头节点不存储任何数据,其中 head 是最上面一层的头节点,跳表的插入,删除以及查找都是从他开始的,如果想要获取下面一层的头节点,可以通过 head.down 获取。跳表的插入总共分为 3 步:

第一步:在跳表节点插入之前先判断上面的层级有没有创建,如果没有创建,需要先创建,如下图所示。

 第二步:如果创建了层级或者插入的层级小于跳表的层数,需要找到每一层待插入节点的前一个节点,如下图所示。

 第三步:从上往下插入节点,链表的插入可以参考单向链表,插入的节点除了连接 next 指针以外,还要连接 down 指针。

 

来看下代码:

 public void add(int num) {
     int level = randLevel();// 从第几层开始插入,随机数。
     // 记录待插入节点的前一个节点。
     SkipListNode[] preNodes = new SkipListNode[level];
 
     // 第一步:如果跳表层数比较少,在上面添加,层数至少为 level 。
     if (curLevelCount < level) {
         SkipListNode beforeHead = head;
         head = new SkipListNode(-1, null);// 更新 head 节点。
         SkipListNode curHead = head;
         // 在上面添加每层的头节点。
         for (int i = curLevelCount; i < level - 1; i++) {
             SkipListNode node = new SkipListNode(-1, null);
             curHead.down = node;
             curHead = node;
        }
         // 最后创建的链表头节点和之前的头节点连在一起。
         curHead.down = beforeHead;
    }
 
     // 第二步:从上往下查找每层待插入节点的前一个节点。
     SkipListNode pre = head;
     // 上层不需要插入的跳过。
     for (int i = curLevelCount - 1; i >= level; i--)
         pre = pre.down;
     // 从当前层往下每层都要插入该节点,找出每层待插入节点的前一个节点。
     for (int i = level - 1; i >= 0; i--) {
         while (pre.next != null && pre.next.val < num)
             pre = pre.next;
         preNodes[i] = pre;// 记录前一个节点。
         pre = pre.down;
    }
 
     // 第三步:节点插入,插入的时候不光有 next 指针,而且还有 down 指针。
     SkipListNode topNode = null;
     // 把新建节点 node 插到该层下面的每一层。
     for (int i = level - 1; i >= 0; i--) {
         // 创建新节点。
         SkipListNode node = new SkipListNode(num, preNodes[i].next);
         // 链表的插入,参见单向链表的插入。
         preNodes[i].next = node;
         // 上下也要连接。
         if (topNode != null)
             topNode.down = node;
         topNode = node;
    }
     if (level > curLevelCount)// 更新跳表的层级,用来记录当前跳表的层级。
         curLevelCount = level;
 }

跳表的查询

跳表的查询从最上面一层开始,每层往后查找,如果后面没有节点了或者后面的节点值比要查找的大,就往下面查找,如果找到返回 true ,如果没找到返回 false ,如下图所示。

 

来看下代码:

 // 查找值为 target 的节点。
 public boolean search(int target) {
     SkipListNode pre = head;
     while (pre != null) {
         // 如果当前节点值小于 target ,需要到右边查找,如果右边没有节点就到下边查找。
         if (pre.val < target) {
             if (pre.next == null)// 右边没有节点,到下边查找
                 pre = pre.down;
             else
                 pre = pre.next.val > target ? pre.down : pre.next;
        } else if (pre.val == target) {// 如果找到直接返回。
             return true;
        } else {
             return false;// 如果当前节点值大于 target ,说明没有,直接返回 false 。
        }
    }
     return false;
 }

跳表节点的删除

节点的删除和添加类似,也是先从上往下找到待删除节点的前一个节点,因为单链表中节点的添加和删除都需要前一个节点。找到之后从当前层往下每一层都要删除,如果上面一层的节点都被删除完了,还需要把上层的链表清空,如下图所示。

 

来看下代码:

public boolean remove(int num) {
     // 删除链表和插入链表类似,都是需要先找到插入或删除链表的前一个节点。
     int topIndex = -1;// 从当前层开始往下每层都要删除。
     // 查找待删除节点的前一个节点,从上面一层开始查找。
     SkipListNode pre = head;
     for (int i = curLevelCount - 1; i >= 0; i--) {
         while (pre.next != null && pre.next.val < num)
             pre = pre.next;
         // 如果找到就终止查找,表示在当前层以及他下面的所有层都要删除该节点。
         if (pre.next != null && pre.next.val == num && topIndex == -1) {
             topIndex = i;
             break;
        }
         if (pre.down == null)// 如果跳表中没有要删除的节点,返回 false 。
             return false;
         pre = pre.down;// 当前层没找到就往下一层继续查找。
    }
 
     if (topIndex == -1)// 如果跳表中没找到要删除的节点,返回 false 。
         return false;
 
     // 从 topIndex 层开始,他下面的每一层都要删除。
     for (int i = topIndex; i >= 0; i--) {
         if (pre.next != null)// 节点删除,参考单向链表删除。
             pre.next = pre.next.next;
         pre = pre.down;// 继续下一层的删除。
         if (pre != null)// 找到待删除节点的前一个节点。
             while (pre.next != null && pre.next.val != num)
                 pre = pre.next;
    }
     // 如果上面一层的节点被删除完了,要更新 curLevelCount 的值 ,还要更新 head节点。
     SkipListNode cur = head;
     while (curLevelCount > 1 && cur.next == null) {
         cur = cur.down;
         head = cur;
         curLevelCount--;
    }
     return true;
 }

最后我们再来看下完整代码:

public class SkipList1 {
 
     // 打印跳表。
     private static void printLinked(SkipList1 skipList) {
         SkipListNode cur = skipList.head;
         while (cur != null) {
             SkipListNode tmp = cur.next;
             while (tmp != null) {
                 System.out.print(tmp.val + ",");
                 tmp = tmp.next;
            }
             System.out.println();
             cur = cur.down;
        }
    }
 
     // 跳表的最大层数。
     private int MAX_LEVEL = 16;
 
     // 当前跳表的最大层级。
     private int curLevelCount = 1;
 
     // 初始化定义一个头节点,指向最上面一层的第一个元素,头节点不存储任何数据。
     private SkipListNode head;
 
     public SkipList1() {// 构造函数。
         head = new SkipListNode(-1, null);
    }
 
     // 查找值为 target 的节点。
     public boolean search(int target) {
         SkipListNode pre = head;
         while (pre != null) {
             // 如果当前节点值小于 target ,需要到右边查找,如果右边没有节点就到下边查找。
             if (pre.val < target) {
                 if (pre.next == null)// 右边没有节点,到下边查找
                     pre = pre.down;
                 else
                     pre = pre.next.val > target ? pre.down : pre.next;
            } else if (pre.val == target) {// 如果找到直接返回。
                 return true;
            } else {
                 return false;// 如果当前节点值大于 target ,说明没有,直接返回 false 。
            }
        }
         return false;
    }
 
     public void add(int num) {
         int level = randLevel();// 从第几层开始插入,随机数。
         // 记录待插入节点的前一个节点。
         SkipListNode[] preNodes = new SkipListNode[level];
 
         // 第一步:如果跳表层数比较少,在上面添加,层数至少为 level 。
         if (curLevelCount < level) {
             SkipListNode beforeHead = head;
             head = new SkipListNode(-1, null);// 更新 head 节点。
             SkipListNode curHead = head;
             // 在上面添加每层的头节点。
             for (int i = curLevelCount; i < level - 1; i++) {
                 SkipListNode node = new SkipListNode(-1, null);
                 curHead.down = node;
                 curHead = node;
            }
             // 最后创建的链表头节点和之前的头节点连在一起。
             curHead.down = beforeHead;
        }
 
         // 第二步:从上往下查找每层待插入节点的前一个节点。
         SkipListNode pre = head;
         // 上层不需要插入的跳过。
         for (int i = curLevelCount - 1; i >= level; i--)
             pre = pre.down;
         // 从当前层往下每层都要插入该节点,找出每层待插入节点的前一个节点。
         for (int i = level - 1; i >= 0; i--) {
             while (pre.next != null && pre.next.val < num)
                 pre = pre.next;
             preNodes[i] = pre;// 记录前一个节点。
             pre = pre.down;
        }
 
         // 第三步:节点插入,插入的时候不光有 next 指针,而且还有 down 指针。
         SkipListNode topNode = null;
         // 把新建节点 node 插到该层下面的每一层。
         for (int i = level - 1; i >= 0; i--) {
             // 创建新节点。
             SkipListNode node = new SkipListNode(num, preNodes[i].next);
             // 链表的插入,参见单向链表的插入。
             preNodes[i].next = node;
             // 上下也要连接。
             if (topNode != null)
                 topNode.down = node;
             topNode = node;
        }
         if (level > curLevelCount)// 更新跳表的层级,用来记录当前跳表的层级。
             curLevelCount = level;
    }
 
     public boolean remove(int num) {
         // 删除链表和插入链表类似,都是需要先找到插入或删除链表的前一个节点。
         int topIndex = -1;// 从当前层开始往下每层都要删除。
         // 查找待删除节点的前一个节点,从上面一层开始查找。
         SkipListNode pre = head;
         for (int i = curLevelCount - 1; i >= 0; i--) {
             while (pre.next != null && pre.next.val < num)
                 pre = pre.next;
             // 如果找到就终止查找,表示在当前层以及他下面的所有层都要删除该节点。
             if (pre.next != null && pre.next.val == num && topIndex == -1) {
                 topIndex = i;
                 break;
            }
             if (pre.down == null)// 如果跳表中没有要删除的节点,返回 false 。
                 return false;
             pre = pre.down;// 当前层没找到就往下一层继续查找。
        }
 
         if (topIndex == -1)// 如果跳表中没找到要删除的节点,返回 false 。
             return false;
 
         // 从 topIndex 层开始,他下面的每一层都要删除。
         for (int i = topIndex; i >= 0; i--) {
             if (pre.next != null)// 节点删除,参考单向链表删除。
                 pre.next = pre.next.next;
             pre = pre.down;// 继续下一层的删除。
             if (pre != null)// 找到待删除节点的前一个节点。
                 while (pre.next != null && pre.next.val != num)
                     pre = pre.next;
        }
         // 如果上面一层的节点被删除完了,要更新 curLevelCount 的值 ,还要更新 head节点。
         SkipListNode cur = head;
         while (curLevelCount > 1 && cur.next == null) {
             cur = cur.down;
             head = cur;
             curLevelCount--;
        }
         return true;
    }
 
     // 索引层级随机函数。
     private int randLevel() {
         int level = 1;
         // Math.random()每次会生成一个 0 到 1 之间的随机数
         while (Math.random() < 0.5f && level < MAX_LEVEL)
             level++;
         return level;
    }
 
 
     // 跳表的节点类。
     public class SkipListNode {
         // 跳表节点的值,在实际应用中节点类可以加个泛型,这里为了方便介绍,直接使用 int 类型。
         public int val;
         public SkipListNode next;// 指向后面一个节点。
         public SkipListNode down;// 指向下面一层的相同节点。
 
         public SkipListNode(int val, SkipListNode next) {
             this.val = val;
             this.next = next;
        }
    }
 }

上面代码中虽然上下两个节点的值是一样的,但我们还是创建了不同的对象,实际上我们只需要创建一个对象即可,上下关系不在是 down ,而是一个数组。

 

来看下代码:

public class SkipList2 {

// 打印跳表的值,由于level是随机生成的,所以同样的数据每次打印都会不一样,
// 如果想调试,可以让level变成一个固定值,这样同样的数据每次打印结果都会一样了。
private static void printLinked(SkipList2 skipList) {
    SkipListNode cur = skipList.head;
    for (int i = skipList.curLevelCount - 1; i >= 0; i--) {
        // 这里是横向查找,就是当前层往后面查找,这里是第 i 层。
        SkipListNode pre = cur;
        while (pre.next[i] != null) {
            System.out.print(pre.next[i].val + ",");
            pre = pre.next[i];
        }
        System.out.println();
    }

}

// 跳表的最大层数。
private int MAX_LEVEL = 16;

// 当前跳表的最大层级。
private int curLevelCount = 1;

// 初始化定义一个头节点,指向最上面一层的第一个元素,头节点不存储任何数据。
private SkipListNode head;

public SkipList2() {// 构造函数。
    head = new SkipListNode(-1);
}

// 查找值为 target 的节点。
public boolean search(int target) {
    SkipListNode pre = head;
    // 这里for循环是逆序的,他是从最上面一层开始查找,for 循环是纵向查找,里面的while循环是横向查找。
    for (int i = curLevelCount - 1; i >= 0; i--) {
        // 这里是横向查找,就是当前层往后面查找,这里是第 i 层。
        while (pre.next[i] != null && pre.next[i].val < target)
            pre = pre.next[i];
        // 如果在当前层查找到,直接返回true。
        if (pre.next[i] != null && pre.next[i].val == target)
            return true;
    }
    return false;// 没查找到,返回false。
}

// 添加数据
public void add(int num) {
    int level = randLevel();// 需要插入第几层。
    // 每一层初始化默认为head节点。
    SkipListNode[] preNodes = new SkipListNode[level];
    for (int i = 0; i < level; i++)
        preNodes[i] = head;

    // 找到每一层待插节点的前一个节点。
    SkipListNode pre = head;
    // 从当前层往下每层都要插入该节点。
    for (int i = level - 1; i >= 0; i--) {
        while (pre.next[i] != null && pre.next[i].val < num)
            pre = pre.next[i];
        preNodes[i] = pre;
    }

    // 创建新节点。
    SkipListNode node = new SkipListNode(num);
    // 把新建节点 node 插到该层以及下面的所有层。
    for (int i = level - 1; i >= 0; i--) {
        // 链表的插入,参见单向链表的插入。
        node.next[i] = preNodes[i].next[i];
        preNodes[i].next[i] = node;
    }
    if (level > curLevelCount)// 更新跳表的层级。
        curLevelCount = level;
}

public boolean remove(int num) {
    // 删除链表和插入链表类似,都是需要先找到插入或删除链表的前一个节点。
    SkipListNode[] preNodes = new SkipListNode[curLevelCount];
    int topIndex = -1;// 从当前层往下都要移除。
    // 查找待删除节点的前一个节点,从最上面一层开始查找。
    SkipListNode pre = head;
    for (int i = curLevelCount - 1; i >= 0; i--) {
        while (pre.next[i] != null && pre.next[i].val < num)
            pre = pre.next[i];
        if (pre.next[i] != null && pre.next[i].val == num && topIndex == -1)
            topIndex = i;
        preNodes[i] = pre;// 记录每层待删除节点的前一个节点。
    }
    if (topIndex == -1)// 如果没找到,也就是待删除的节点在跳表中不存在,直接返回。
        return false;

    // 删除操作。
    for (int i = topIndex; i >= 0; i--)
        preNodes[i].next[i] = preNodes[i].next[i].next[i];
    // 更新索引层数,如果当前层消失了,curLevelCount要减 1 。
    while (curLevelCount > 1 && head.next[curLevelCount - 1] == null)
        curLevelCount--;
    return true;
}

// 索引层级随机函数。
private int randLevel() {
    int level = 1;// 1 的概率是0.5,2的概率是0.25,3的概率是0.125,4的概率是0.0625,……
    // Math.random()每次会生成一个 0 到 1 之间的随机数
    while (Math.random() < 0.5f && level < MAX_LEVEL)
        level++;
    return level;
}


// 跳表的节点。
public class SkipListNode {
    // 跳表节点的值,在实际应用中节点类可以加个泛型,这里为了方便介绍,直接使用 int 类型。
    public int val;

    public SkipListNode(int val) {
        this.val = val;
    }

    // 普通的链表这里是 next 节点,而跳表这里需要记录每一层的节点,所以是数组。
    public SkipListNode[] next = new SkipListNode[MAX_LEVEL];
}

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

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

相关文章

chatgpt赋能python:Python如何依次取字符——一种简单有效的方法

Python如何依次取字符——一种简单有效的方法 1. 介绍 Python 常常被用于编写文本处理脚本&#xff0c;而文本处理中的一个常见任务就是依次取字符。本文将介绍一种简单高效的方法&#xff0c;让您可以在 Python 中便捷地完成此操作。 2. 如何依次取字符 Python 中的字符串…

黑客入门必备指南

在探讨黑客如何入门之前&#xff0c;首先我们的思想要端正。 作为一名黑客&#xff0c;必须要有正直善良的价值观。 或许你听过这么一句话“能力越大&#xff0c;责任越大”作为一名黑客就是如此&#xff0c;黑客的技术越精湛&#xff0c;能力就越大&#xff0c;就越不能去干…

spark入门 Linux模式 Local模式 (二)

一、下载对应的spark包 https://archive.apache.org/dist/spark/spark-3.0.0/ 我这里下载的是spark-3.0.0-bin-hadoop3.2.tgz 二、解压 tar -zvxf spark-3.0.0-bin-hadoop3.2.tgz三、启动 再解压路径的bin目录执行下 ./spark-shell 四、测试 WordCount代码例子 sc.textFil…

接口测试-使用mock生产随机数据

在做接口测试的时候&#xff0c;有的接口需要进行大量的数据进行测试&#xff0c;还不能是重复的数据&#xff0c;这个时候就需要随机生产数据进行测试了。这里教导大家使用mock.js生成各种随机数据。 一、什么是mock.js mock.js是用于生成随*机数据&#xff0c;拦截 Ajax 请…

uniapp引入uView正确步骤及误区

1.导入uview组件库 2.导入成功后在main.js里引入 import uView from /uni_modules/uview-ui Vue.use(uView)3.在App.vue里引入样式文件 import "/uni_modules/uview-ui/index.scss";4.在pages.json里添加配置 "easycom": {"^u-(.*)": "/…

大聪明教你学Java | parallelStream().forEach() 的踩坑日记

前言 &#x1f34a;作者简介&#xff1a; 不肯过江东丶&#xff0c;一个来自二线城市的程序员&#xff0c;致力于用“猥琐”办法解决繁琐问题&#xff0c;让复杂的问题变得通俗易懂。 &#x1f34a;支持作者&#xff1a; 点赞&#x1f44d;、关注&#x1f496;、留言&#x1f4…

springboot学生管理系统(含源码+数据库)

本次系统开发所用到的Java语言、Spring框架、SpringMVC框架、MyBatis框架、SpringBoot框架以及MySQL。 1.系统分析 &#xff08;1&#xff09;教师管理需求&#xff0c;学校想轻松的查阅指定教师的信息&#xff0c;学校对教师进行一个基本的信息管理&#xff0c;学校可以方便…

【python】脚本编写

这里写自定义目录标题 欢迎使用python来编写脚本环境搭建 欢迎使用python来编写脚本 测试方向&#xff0c;测试报告&#xff0c;单元测试 环境搭建 python环境搭建 下载地址 https://www.python.org/ 文档 https://docs.python.org/3/ pycharm的环境 使用chatgpt来实现代码功…

【安全架构】

概念 安全是产品的属性&#xff0c;安全的目标是保障产品里信息资产的保密性&#xff08;Confidentiality&#xff09;、完整性&#xff08;Integrity&#xff09;和可用性&#xff08;Availability&#xff09;&#xff0c;简记为CIA。 保密性&#xff1a; 保障信息资产不被未…

通过Visual Studio诊断工具定位软件CPU瓶颈

通过VS诊断工具定位软件CPU瓶颈 前情提示&#xff1a;正常情况下我们使用调试模式会看不到诊断工具窗口&#xff0c;控制台会报“无法启动标准收集器。请尝试修复 Visual Studio 的安装。 (HRESULT: 0xe1110002)”这样的错误。 解决方式&#xff1a;通过[Downloads - Visual St…

00后是太恐怖了,工作没两年,跳槽到我们公司起薪20K都快接近我了

在程序员职场上&#xff0c;什么样的人最让人反感呢? 是技术不好的人吗?并不是。技术不好的同事&#xff0c;我们可以帮他。 是技术太强的人吗?也不是。技术很强的同事&#xff0c;可遇不可求&#xff0c;向他学习还来不及呢。 真正让人反感的&#xff0c;是技术平平&…

【JAVA】---逆波兰表达式

一. 逆波兰表达式的介绍 逆波兰表达式又称为后缀表达式&#xff0c;代表的含义是操作数在前&#xff0c;运算符在后。 比如&#xff1a;12&#xff0c;用逆波兰表达式来写的话&#xff0c;就是12。 而12这种写法称为中缀表达式&#xff0c;即运算符在两个操作数之间&#xff0c…

Office Visio 2019安装教程

哈喽&#xff0c;大家好。今天一起学习的是Visio 2019的安装&#xff0c;这是一个绘制流程图的软件&#xff0c;用有效的绘图表达信息&#xff0c;比任何文字都更加形象和直观。Office Visio 是office软件系列中负责绘制流程图和示意图的软件&#xff0c;便于IT和商务人员就复杂…

测试老鸟总结,自动化测试难点挑战应对方法,我的进阶之路...

目录&#xff1a;导读 前言一、Python编程入门到精通二、接口自动化项目实战三、Web自动化项目实战四、App自动化项目实战五、一线大厂简历六、测试开发DevOps体系七、常用自动化测试工具八、JMeter性能测试九、总结&#xff08;尾部小惊喜&#xff09; 前言 Python自动化测试&…

Redis 高级数据结构 HyperLogLog

介绍 HyperLogLog(Hyper[ˈhaɪpə(r)])并不是一种新的数据结构(实际类型为字符串类型)&#xff0c;而是一种基数算法,通过HyperLogLog可以 利用极小的内存空间完成独立总数的统计&#xff0c;数据集可以是IP、Email、ID等。如果你负责开发维护一个大型的网站&#xff0c;有一天…

Vue.js 如何进行打包部署

Vue.js 中的打包部署 Vue.js 是一款流行的前端框架&#xff0c;它提供了一种简单、灵活的方式来构建用户界面。在开发完成后&#xff0c;需要对 Vue.js 应用程序进行打包部署&#xff0c;以便在生产环境中使用。本文将介绍 Vue.js 中的打包部署以及如何进行打包部署。 打包部署…

运维小白必学篇之基础篇第十七集:NFS和DHCP实验

NFS和DHCP实验 目录 NFS和DHCP实验 环境配置&#xff1a; 实验题1&#xff1a;实现NFS服务 实验题2&#xff1a;实现DHCP服务 实验作业&#xff1a; 计算机1配置如下&#xff1a;&#xff08;计算机名为姓名首拼&#xff0c;例hy01&#xff0c;hy02...&#xff09;基础环…

为什么初学者都先学C语言?

不少高校选择C语言&#xff0c;主要C语言是一种相对底层的语言&#xff0c;学习它可以让学习者更好的理解计算机的基本原理和编程的基础概念&#xff0c;比如变量、函数、指针等。这些基础知识对于理解其他高级语言和解决复杂的编程问题都非常重要。 另外就是C语言对算法和数据…

最小化微服务漏洞

最小化微服务漏洞 目录 本节实战 实战名称&#x1f498; 案例&#xff1a;设置容器以普通用户运行-2023.5.29(测试成功)&#x1f498; 案例&#xff1a;避免使用特权容器&#xff0c;选择使用capabilities-2023.5.30(测试成功)&#x1f498; 案例&#xff1a;只读挂载容器文件…