TypeScript 哈希表

news2024/11/25 2:42:57

文章目录

  • 概念
    • 哈希化
    • 冲突
      • 链地址法
      • 开放地址法
    • 装填因子(loadFactor)
    • 效率对比
    • 哈希函数
      • 字符串转数字算法 —— 幂的连乘
      • 压缩数字范围 —— 取余
      • 优秀哈希算法的优点
      • 快速计算:霍纳法则
      • 均匀分布 —— 质数
      • Java 中的 HashMap
      • N次幂的底数
  • 实现
    • 哈希函数
    • 哈希表
      • 插入/更新操作
      • 获取
      • 删除
      • 扩容、缩容
      • 判断一个数是否为质数
    • 完整代码

概念

哈希表通常底层实现是一个数组,它的特点就是弥补了数组查找、插入、删除慢的缺点。哈希表它就是最终存放数据的数组。但我们平时说哈希表,确切说是一种操作数组的方式,一种算法。

哈希表巧妙的地方就在于转换了思维角度。
之前搜索一个东西,我们不知道它位置,所有要一个一个找,为啥不知道位置?因为放的时候是随机放的。而哈希表是放的时候不允许随机放,直接就规定了位置,那去找的时候,不就按位置直接去拿即可。比如你知道苹果一定放第三个格子,现在你要找苹果,就会直奔第三个格子,而不是从第一个格子找起。

哈希化

那怎么确定苹果就是放第三个格子呢?
这就需要哈希函数来映射了,这个过程称为哈希化。

image.png

理想情况三个水果,算出来就放在三个对应的格子里,但现实很骨感。哈希函数的映射算法没这么完美,会出现只有三个水果,但算出来一个水果要放在第1000个格子的情况,因此中间会出现很多空格子浪费了。优化哈希算法,使之不要这么浪费是衡量哈希算法优劣的一个重要指标。

冲突

那会不会出现哈希函数设计不好,算出来苹果和梨都被放到第三个格子的情况呢?
存在的,通常有两种方式解决这个问题。

链地址法

可以在第三个格子里保存一个链表或数组,然后把苹果和梨都挂在链表上。这种方式称为链地址法。当然挂一个数组也可以,如果重复的数据实在太多,还可以挂一个二叉树。

套娃的这个链表或者数组,我们通常称为打水的吊桶 bucket。

有个误区要注意:设计哈希表的时候要明白,存储过程不是这个位置本是没内容的,然后放进一个内容,直到此处出现了冲突,这个位置才开始挂一个桶,然后新旧内容一起都放桶里。而是哈希表生成的时候,确实没内容,为 undefined,但第一个元素放进去的时候,此处就会生成一个桶存放数据,不用等到发生冲突才开始换成桶。

image.png

开放地址法

还可以让重复的元素去后面找空白的位置坐下,后面还有空位,老弟往后走。这是开发地址法。

至于怎么找后面的空位,又有三种方式:

  1. 线性探测:一个一个往后找
  2. 二次探测:也是线性探测,但是不是一个一个找,而是步长为 2,2、4、8… 跳着找。
  3. 再次hash

经过前人的验证,总的来说,链地址法效率是更高的,所以链地址法也用的最多。

装填因子(loadFactor)

装填因子是数据量与哈希表数组容量的比值,也就是哈希表装的满不满。
整这么个概念,主要是为哈希表的性能优化做一个指标以及作为是否自动扩容的依据。

我们知道完美的哈希表,装填因子应该是 1,三个水果,就放三个格子,没浪费没冲突。

但冲突和浪费都不可避免,开发地址法装填因子不会超过1,虽然它很好的利用了格子,可是多了找新地址的计算。
链地址法装填因子可能超过 1,装的数据比哈希表数组长度还多,因为它可以在里面挂一个超长的链。当然挂太多,效率肯定不高。

  • 一般装填因子小于 0.25 需要缩容,大于 0.75 需要扩容。

效率对比

下面的等式显示了线性探测时,探测序列§和填装因子(L)的关系。公式来自于Knuth(算法分析领域的专家,现代计算机的先驱人物)。

线性探测二次探测和再哈希化链地址法

经过上面的比较我们可以发现,链地址法相对来说效率是好于开放地址法的。

所以在真实开发中,使用链地址法的情况较多。因为它不会因为添加了某元素后性能急剧下降。比如在Java的HashMap中使用的就是链地址法。

哈希函数

说白了哈希化,也就是哈希函数的功能就是将字符串转成对应的数组下标,也就是数字。

那具体怎么做呢?

字符串转数字算法 —— 幂的连乘

现在我们需要设计一种方案,可以将单词转成适当的下标值:
 其实计算机中有很多的编码方案就是用数字代替单词的字符。就是字符编码。(常见的字符编码?)
 比如ASCII编码:a是97,b是98,依次类推122代表z
 我们也可以设计一个自己的编码系统,比如a是1,b是2,c是3,依次类推,z是26。
 当然我们可以加上空格用0代替,就是27个字符(不考虑大写问题)
 但是,有了编码系统后,一个单词如何转成数字呢?

方案一:数字相加
 一种转换单词的简单方案就是把单词每个字符的编码求和。
 例如单词cats转成数字:3+1+20+19=43,那么43就作为cats单词的下标存在数组中。

◼ 问题:按照这种方案有一个很明显的问题就是很多单词最终的下标可能都是43。
 比如was/tin/give/tend/moan/tick等等。
 我们知道数组中一个下标值位置只能存储一个数据
 如果存入后来的数据,必然会造成数据的覆盖。
 一个下标存储这么多单词显然是不合理的。
 虽然后面的方案也会出现,但是要尽量避免。

方案二:幂的连乘
 现在,我们想通过一种算法,让cats转成数字后不那么普通。
 数字相加的方案就有些过于普通了。
 有一种方案就是使用幂的连乘,什么是幂的连乘呢?
 其实我们平时使用的大于10的数字,可以用一种幂的连乘来表示它的唯一性:比如:7654 = 710³+610²+510+4
 我们的单词也可以使用这种方案来表示:比如 cats = 3
27³+127²+2027+17= 60337
 这样得到的数字可以基本保证它的唯一性,不会和别的单词重复。

两种方案总结:
 第一种方案(把数字相加求和)产生的数组下标太少。
 第二种方案(与27的幂相乘求和)产生的数组下标又太多。

第一种方案缺陷无解,第二种方案,我们可以采用数字压缩算法缓解。

压缩数字范围 —— 取余

除以几,就能把数字压缩到[0,这个数字 - 1]的范围。比如除以 10,就压缩到了 [0, 9] 的范围。

优秀哈希算法的优点

两点:

  1. 快速的计算:计算 hashcode 要快。
    • 提高速度的一个办法就是让哈希函数中尽量少的有乘法和除法。因为它们的性能是比较低的。
  2. 分布均匀,也就是冲突少。

前面我们已经知道,幂的连乘 和 取余操作实现哈希算法。但这还不够,还可以继续优化。

快速计算:霍纳法则

霍纳法则可以减少多项式中的乘法,转换成加法。

做法就是一直在提取公因式,提到不能再提为止。

以 abc 为例:
原本公式:a的编码 * 31^2 + b的编码 * 31^1 + c的编码 * 31^0

公因式就是幂底:31

霍纳法则:

  1. 31*(a*31 + b) + c
  2. 31*(31*(31*0 + a) + b) + c

上面提取到第二次已经无法再提了,用语言描述就是 hashcode 从 0 开始与幂底的积再加上字符串第一个字符的编码的和作为下一次的 hashcode,继续乘幂底与第二个字符的编码的和再次作为 hashcode 进入下一轮循环,直到加完所有的字符。

可以见到一个循环即可完成霍纳算法对多项式的计算。

const POWER_BASE = 31;
let hashcode = 0;
// 霍纳法则,计算hash值
for (let i = 0; i < key.length; i++) {
  hashcode = hashcode * POWER_BASE + key.charCodeAt(i);
}

霍纳法则

image.png

均匀分布 —— 质数

在设计哈希表时,我们已经有办法处理映射到相同下标值的情况:链地址法或者开放地址法。但是无论哪种方案,为了提供效率,最好的情况还是让数据在哈希表中均匀分布。

因此,我们需要在使用常量的地方,尽量使用质数。

质数的使用:

  • 哈希表的长度。
  • N次幂的底数(我们之前使用的是27)

为什么他们使用质数,会让哈希表分布更加均匀呢?
 质数和其他数相乘的结果相比于其他数字更容易产生唯一性的结果,减少哈希冲突。
 Java中的N次幂的底数选择的是31,是经过长期观察分布结果得出的;

Java 中的 HashMap

◼ Java中的哈希表采用的是链地址法。

◼ HashMap的初始长度是16,每次自动扩展,长度必须是2的次幂。
 这是为了服务于从Key映射到index的算法。60000000 % 100 = 数字。下标值

◼ HashMap中为了提高效率,采用了位运算的方式。
 HashMap中index的计算公式:index = HashCode(Key) & (Length - 1)
 比如计算book的hashcode,结果为十进制的3029737,二进制的101110001110101110 1001
 假定HashMap长度是默认的16,计算Length-1的结果为十进制的15,二进制的1111
 把以上两个结果做与运算,101110001110101110 1001 & 1111 = 1001,十进制是9,所以 index=9

为什么 Java hashmap 中使用的数组长度不是质数,因为它使用了位运算,而不是取模。

N次幂的底数

◼ 这里采用质数的原因是为了产生的数据不按照某种规律递增。
 比如我们这里有一组数据是按照4进行递增的:0 4 8 12 16,将其映射到长度为8的哈希表中。
 它们的位置是多少呢?0 - 4 - 0 - 4,依次类推。
 如果我们哈希表本身不是质数,而我们递增的数量可以使用质数,比如5,那么 0 5 10 15 20
 它们的位置是多少呢?0 - 5 - 2 - 7 - 4,依次类推。也可以尽量让数据均匀的分布。
 我们之前使用的是27,这次可以使用一个接近的数,比如31/37/41等等。一个比较常用的数是31或37。

◼ 总之,质数是一个非常神奇的数字。

◼ 这里建议两处都使用质数:
 哈希表中数组的长度。
 N次幂的底数。

实现

哈希函数

/**
 * 哈希函数, 将key映射成index
 * @param key 转换的key
 * @param capacity 容量 (数组的长度)
 * @returns index 索引值
 */
export function hashFun(key: string, capacity: number): number {
    const POWER_BASE = 31;
    let hashcode = 0;
    // 霍纳法则,计算hash值
    for (let i = 0; i < key.length; i++) {
        hashcode = hashcode * POWER_BASE + key.charCodeAt(i);
    }
    // 取余压缩范围
    return hashcode % capacity;
}

哈希表

export class HashTable<T = any> {
    // 哈希主表数组,采用拉链法,元素本是链表,这里以数组代替,并且元素类型为元祖,便于将 key 和 value 都存一起。
    // 简言之数组元素还是数组,这个元素数组里元素为元祖类型。
    private store: [string, T][][] = [];

    // 装填因子:自动扩容依据
    
    // 容量
    private capacity: number = 7;
    // 装载数
    private count: number = 0;


    // 哈希函数
    private hashFun(key: string, capacity: number): number {
        const POWER_BASE = 31;
        let hashcode = 0;
        // 霍纳法则,计算hash值
        for (let i = 0; i < key.length; i++) {
            hashcode = hashcode * POWER_BASE + key.charCodeAt(i);
        }
        // 取余压缩范围
        return hashcode % capacity;
    }
}

插入/更新操作

哈希表的插入和修改操作是同一个函数:
 因为,当使用者传入一个<Key,Value>时
 如果原来不存该key,那么就是插入操作。
 如果已经存在该key,那么就是修改操作。

// 插入/更新
put(key: string, value: T) {
    const index = this.hashFun(key, this.capacity);
    // 获取对应下标的数组
    const bucket = this.store[index];

    if (!bucket) {
        // 插入
        this.store[index] = [[key, value]];
        this.count++;
    } else {
        for (let i = 0; i < bucket.length; i++) {
            const tuple = bucket[i];
            if (tuple[0] === key) {
                // 更新
                tuple[1] = value;
                return;
            }
        }
      	// 插入
        bucket.push([key, value]);
        this.count++;
    }
}

获取

// 获取值
get(key: string): T | null {
    const index = this.hashFun(key, this.capacity);
    const bucket = this.store[index];

    if (bucket) {
        for (let i = 0; i < bucket.length; i++) {
            const tuple = bucket[i];
            if (tuple[0] === key) {
                return tuple[1];
            }
        }
    }
    return null;
}

删除

// 删除
remove(key: string): T | null {
    const index = this.hashFun(key, this.capacity);
    const bucket = this.store[index];

    if (bucket) {
        for (let i = 0; i < bucket.length; i++) {
            const tuple = bucket[i];
            if (tuple[0] === key) {
                bucket.splice(i, 1);
                this.count--;
                return tuple[1];
            }
        }
    }
    return null;
}

扩容、缩容

修改容量有两步操作:

  1. 修改容量
  2. 把之前的数据再次哈希化计算一次,放进新数组
private MIN_CAPACITY = 7;
private MIN_LOAD_FACTOR = 0.25;
private MAX_LOAD_FACTOR = 0.75;
private capacity: number = this.MIN_CAPACITY;

private resize(newCapacity: number) {
    
    const oldStore = this.store;

    // 1. 构建新数组
    this.store = [];
    this.capacity = newCapacity < this.MIN_CAPACITY ? this.MIN_CAPACITY : newCapacity;
    this.count = 0;

    // 2. 旧数据哈希化放入新数组
    oldStore.forEach(bucket => {
        if (bucket) {
            for (let i = 0; i < bucket.length; i++) {
                const tuple = bucket[i];
                this.put(tuple[0], tuple[1]);
            }
        }
    });
}

修改插入和删除方法:

// 插入/更新
put(key: string, value: T): void {
    const index = this.hashFun(key, this.capacity);
    // 获取对应下标的数组
    const bucket = this.store[index];

    if (!bucket) {
        // 插入
        this.store[index] = [[key, value]];
        this.count++;
    } else {
        for (let i = 0; i < bucket.length; i++) {
            const tuple = bucket[i];
            if (tuple[0] === key) {
                // 更新
                tuple[1] = value;
                return;
            }
        }
        bucket.push([key, value]);
        this.count++;
    }

    // 自动扩容
    if (this.loadFactor > this.MAX_LOAD_FACTOR) this.resize(this.capacity * 2);
}

// 删除
remove(key: string): T | null {
    const index = this.hashFun(key, this.capacity);
    const bucket = this.store[index];

    if (bucket) {
        for (let i = 0; i < bucket.length; i++) {
            const tuple = bucket[i];
            if (tuple[0] === key) {
                bucket.splice(i, 1);
                this.count--;
                // 缩容
                if (this.loadFactor < this.MIN_LOAD_FACTOR && this.capacity > this.MIN_CAPACITY) {
                    this.resize(Math.floor(this.capacity / 2));
                }
                return tuple[1];
            }
        }
    }
    return null;
}

但现在有个问题,扩容和缩容都是按照 2 倍来做的,那容量不就不是质数了吗,这不利于保证元素均匀分布呀?
是的,因此容量不能简单设为 2 倍,而是应该通过一个算法找附近的质数。

判断一个数是否为质数

质数表示大于1的自然数中,只能被1和自己整除的数。
因此让这个数字 n 从 2 开始除,一直除到小于根号 n 的最大整数即可。如果除尽了,说明 n 不是质数。

怎么判断除没除尽?
用取余,有余数说明没除尽。

/**
 * 判断数字是否为质数
 * @param num 
 * @returns boolean
 */
export function isPrime(num: number) {
    if (num < 2) return false;
    for (let i = 2; i <= Math.sqrt(num); i++) {
        if (num % i === 0) return false;
    }
    return true;
}

保证容量为质数,并且当缩容后的容量小于最小容量时,保持最小容量:

// 判断质数
private isPrime(num: number) {
    if (num < 2) return false;
    for (let i = 2; i <= Math.sqrt(num); i++) {
        if (num % i === 0) return false;
    }
    return true;
}

// 获取质数
private getPrime(num: number) {
    while (!this.isPrime(num)) {
        num++;
    }
    return num;
}

private resize(newCapacity: number) {
    const primeCapacity = this.getPrime(newCapacity);

    const oldStore = this.store;

    this.store = [];
    // 最小容量为底线
    this.capacity = primeCapacity < this.MIN_CAPACITY ? this.MIN_CAPACITY : primeCapacity;
    this.count = 0;

    ...
}

完整代码

  • 注:数组实现的 bucket,并非链表。
export class HashTable<T = any> {
    // 哈希主表数组,采用拉链法,元素本是链表,这里以数组代替,并且元素类型为元祖,便于将 key 和 value 都存一起。
    // 简言之数组元素还是数组,这个元素数组里元素为元祖类型。
    private store: [string, T][][] = [];
    // 容量
    private MIN_CAPACITY = 7;
    private MIN_LOAD_FACTOR = 0.25;
    private MAX_LOAD_FACTOR = 0.75;
    private capacity: number = this.MIN_CAPACITY;
    // 装载数
    private count: number = 0;

    // 装填因子:自动扩容依据
    private get loadFactor() {
        return this.count / this.capacity;
    }

    // 哈希函数
    private hashFun(key: string, capacity: number): number {
        const POWER_BASE = 31;
        let hashcode = 0;
        // 霍纳法则,计算hash值
        for (let i = 0; i < key.length; i++) {
            hashcode = hashcode * POWER_BASE + key.charCodeAt(i);
        }
        // 取余压缩范围
        return hashcode % capacity;
    }

    // 判断质数
    private isPrime(num: number) {
        if (num < 2) return false;
        for (let i = 2; i <= Math.sqrt(num); i++) {
            if (num % i === 0) return false;
        }
        return true;
    }

    // 获取质数
    private getPrime(num: number) {
        while (!this.isPrime(num)) {
            num++;
        }
        return num;
    }

    private resize(newCapacity: number) {
        const primeCapacity = this.getPrime(newCapacity);

        const oldStore = this.store;

        // 1. 构建新数组
        this.store = [];
        this.capacity = primeCapacity < this.MIN_CAPACITY ? this.MIN_CAPACITY : primeCapacity;
        this.count = 0;

        // 2. 旧数据哈希化放入新数组
        oldStore.forEach(bucket => {
            if (bucket) {
                for (let i = 0; i < bucket.length; i++) {
                    const tuple = bucket[i];
                    this.put(tuple[0], tuple[1]);
                }
            }
        });
    }

    // 插入/更新
    put(key: string, value: T): void {
        const index = this.hashFun(key, this.capacity);
        // 获取对应下标的数组
        const bucket = this.store[index];

        if (!bucket) {
            // 插入
            this.store[index] = [[key, value]];
            this.count++;
        } else {
            for (let i = 0; i < bucket.length; i++) {
                const tuple = bucket[i];
                if (tuple[0] === key) {
                    // 更新
                    tuple[1] = value;
                    return;
                }
            }
            bucket.push([key, value]);
            this.count++;
        }

        // 自动扩容
        if (this.loadFactor > this.MAX_LOAD_FACTOR) this.resize(this.capacity * 2);
    }

    // 获取值
    get(key: string): T | null {
        const index = this.hashFun(key, this.capacity);
        const bucket = this.store[index];

        if (bucket) {
            for (let i = 0; i < bucket.length; i++) {
                const tuple = bucket[i];
                if (tuple[0] === key) {
                    return tuple[1];
                }
            }
        }
        return null;
    }

    // 删除
    remove(key: string): T | null {
        const index = this.hashFun(key, this.capacity);
        const bucket = this.store[index];

        if (bucket) {
            for (let i = 0; i < bucket.length; i++) {
                const tuple = bucket[i];
                if (tuple[0] === key) {
                    bucket.splice(i, 1);
                    this.count--;
                    // 缩容
                    if (this.loadFactor < this.MIN_LOAD_FACTOR && this.capacity > this.MIN_CAPACITY) {
                        this.resize(Math.floor(this.capacity / 2));
                    }
                    return tuple[1];
                }
            }
        }
        return null;
    }
}

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

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

相关文章

基于QGIS的研究区域遥感影像裁切下载方法-以岳麓区为例

目录 前言 一、数据说明 1、遥感影像 2、矢量范围 二、按矢量范围导出 1、第一步、导出影像 2、第二步、设置输出格式 3、设置裁切范围 4、设置分辨率 三、按矢量范围掩膜 1、第一步、打开裁剪工具 2、第二步、参数设置 ​编辑 3、执行掩膜 四、webgis支持 1、生成运行…

NineData云原生智能数据管理平台新功能发布|2024年2月版

SQL开发&#xff1a;全功能支持百度云 GaiaDB 介绍&#xff1a;支持通过 SQL 开发所有能力管理 GaiaDB 实例。更多信息&#xff0c;请参见&#xff1a;真香&#xff01;NineData SQL 开发全面适配 GaiaDB 场景&#xff1a;企业使用 GaiaDB 管理企业数据&#xff0c;需要一个一…

【EI会议征稿通知】第三届信息经济、数据建模与云计算国际学术会议 (ICIDC 2024)

第三届信息经济、数据建模与云计算国际学术会议 2024 3rd International Conference on Information Economy, Data Modeling and Cloud Computing&#xff08;ICIDC 2024&#xff09; 第三届信息经济、数据建模与云计算国际学术会议(ICIDC 2024)定于2024年6月21-23日在中国…

javaWebssh药品进销存信息管理系统myeclipse开发mysql数据库MVC模式java编程计算机网页设计

一、源码特点 java ssh药品进销存信息管理系统是一套完善的web设计系统&#xff08;系统采用ssh框架进行设计开发&#xff09;&#xff0c;对理解JSP java编程开发语言有帮助&#xff0c;系统具有完整的源代码和数据库&#xff0c;系统主要采用B/S模式开发。开发环境为TOM…

YOLOv8-Openvino-ByteTrack【CPU】

纯检测如下&#xff1a; YOLOv5-Openvino和ONNXRuntime推理【CPU】 YOLOv6-Openvino和ONNXRuntime推理【CPU】 YOLOv8-Openvino和ONNXRuntime推理【CPU】 YOLOv9-Openvino和ONNXRuntime推理【CPU】 注&#xff1a;YOLOv8和YOLOv9代码内容基本一致&#xff01; 全部代码Github&…

[数据集][图像分类]芒果叶病害分类数据集4000张5类别

数据集格式&#xff1a;仅仅包含jpg图片&#xff0c;每个类别文件夹下面存放着对应图片 图片数量(jpg文件个数)&#xff1a;4000 分类类别数&#xff1a;8 类别名称:["anthracnose","bacterial_canker","cutting_weevil","die_back",&…

EasyRecovery易恢复14最新版Win电脑安装包下载

EasyRecovery易恢复是一款数据恢复软件。它专门用于恢复因各种情况&#xff08;如误删除、格式化、病毒攻击、分区丢失等&#xff09;而丢失的数据。这款软件支持恢复多种类型的文件&#xff0c;包括文档、图片、视频、音频等&#xff0c;并且可以从各种存储设备&#xff08;如…

【YOLO v5 v7 v8 v9小目标改进】HTA:自注意力 + 通道注意力 + 重叠交叉注意力,提高细节识别、颜色表达、边缘清晰度

HTA&#xff1a;自注意力 通道注意力 重叠交叉注意力&#xff0c;提高细节识别、颜色表达、边缘清晰度 提出背景框架浅层特征提取深层特征提取图像重建混合注意力块&#xff08;HAB&#xff09;重叠交叉注意力块&#xff08;OCAB&#xff09;同任务预训练效果 小目标涨点YOLO…

06 - 镜像管理

1 了解镜像 Docker镜像是一个特殊的文件系统&#xff0c;除了提供容器运行时所需的程序、库、资源、配置等文件外&#xff0c;还包含了一些为运行时准备的一些配置参数&#xff08;如匿名卷、环境变量、用户等&#xff09;。 但注意&#xff0c; 镜像不包含任何动态数据&#…

sql多表运用 12.3

肖SIR__数据库之多表运用__12.3 数据库之多表运用 CREATE table dept(dept1 VARCHAR(6),dept_name VARCHAR(20)) default charsetutf8; INSERT into dept VALUES (101,财务); INSERT into dept VALUES (102,销售); INSERT into dept VALUES (103,IT技术); INSERT into dep…

【已解决】conda环境下ROS2 colcon build编译选择特定python解释器

目录 1 问题背景2 问题探索3 问题解决4 告别Bug 1 问题背景 环境&#xff1a; ROS2 HumbleUbuntu22.04 现象&#xff1a;运行colcon build后由cpp编译生成的python导出库(如自定义消息、服务等)&#xff0c;其版本与由python setup.py安装的python库版本不一致&#xff0c;导致…

App自动化测试笔记(四):UIAutomatorViewer与元素定位API

UIAutomatorViewer 1、应用场景 定位元素的时候必须根据元素的相关特征来进行定位&#xff0c;而 UIAutomatorViewer 就是用来获取元素特征的。 如何使用UIAutomatorViewer 1、保证想要查看的元素在当前的频幕上 2、打开UIAutomatorViewer工具 3、点击左上角左数第二个按钮…

IDEA切换JDK版本超详细步骤

&#x1f600; IDEA切换JDK版本详细教程&#xff0c;全网步骤最详细&#xff0c;实测可用。 文章目录 第一步、选择SDKs切换SDK版本&#xff1a;第二步、选择Modules切换Sources和Dependencies版本&#xff1a;第三步、选择Project切换SDK和Language Level版本&#xff1a;第四…

VR科学知识互动展示介绍|游戏体验馆加盟|VR展示厅

VR科学知识互动展示是一种利用虚拟现实技术来呈现科学知识并与观众进行互动的展示方式。通过VR设备&#xff0c;参观者可以沉浸在各种科学主题的虚拟环境中&#xff0c;以全新的视角和体验来探索科学领域的知识。 这样的展示通常结合了视觉、听觉和触觉等感官体验&#xff0c;使…

网络信息安全:11个常见漏洞类型汇总

一、SQL注入漏洞 SQL注入攻击&#xff08;SQL Injection&#xff09;&#xff0c;简称注入攻击、SQL注入&#xff0c;被广泛用于非法获取网站控制权&#xff0c;是发生在应用程序的数据库层上的安全漏洞。 在设计程序&#xff0c;忽略了对输入字符串中夹带的SQL指令的检查&…

恒峰便携式森林消防灭火泵:轻巧易携,灭火无忧

在自然环境中&#xff0c;森林火灾是一种常见的自然灾害。为了保护森林资源和人民生命财产安全&#xff0c;我们需要一款轻便、高效的消防灭火设备。本田发动机单缸四冲手拉式启动的便携式森林消防灭火泵应运而生&#xff0c;它以其出色的性能和便捷的使用方式&#xff0c;成为…

工作中常用的6种设计模式

背景 谈起设计模式&#xff0c;你一定会问&#xff1f;这玩意到底有啥用&#xff1f;我好像没用过也不影响做一个码农。也可能项目中实际用了&#xff0c;但是你不自知。虽然Java设计模式有23种&#xff0c;但是工作中常用的可能并没有那麽多。就像新华字典有多少字&#xff0c…

SpringBoot使用MongoTemplate详解

1.pom.xml引入Jar包 <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-mongodb</artifactId> </dependency> 2.MongoDbHelper封装 /*** MongoDB Operation class* author HyoJung* date …

群晖 NAS 安装 Ghost Blog

通过群晖的 docker 容器下载 ghost 映像&#xff0c;并配置后启动。 一、下载映像文件 由于网络环境&#xff0c;无法直接连接 docker 的映像服务器&#xff0c;因此使用境内代理&#xff0c;通过 url 方式下载。 打开 docker 控制台&#xff0c;点击左边导航栏的 “映像”&…

2024大厂Android面试真题集锦,算法真题解析:美团+Tencent+字节跳动+阿里+360+拼多多

前言 IT行业薪水高&#xff0c;这是众所周知的&#xff0c;所以很多人大学都选择IT相关专业&#xff0c;即使非该专业的人&#xff0c;毕业了也想去一个培训机构镀镀金&#xff0c;进入这一行业。 但是有关这个行业35岁就退休的说法&#xff0c;也一直盛传。 加上这几年不断…