Java中BitSet和Set统计不重复字符数量时间复杂度和空间复杂度分析

news2024/9/29 7:30:50

题目:HJ10 字符个数统计

牛客网上一道简单的题目,计算字符串中含有的不同字符的个数,一看这个题目,通常直接意识到方案的基本实现思路:设置一个容器,遍历字符串中的每个字符,若字符与容器中字符相同,则不添加,否则加入,最后统计容器字符的个数;
Java语言开发,很容易想到通过Set容器剔除,所以有下列的实现

方案一:通过SET剔除重复实现

import java.util.Scanner;
import java.util.*;
// 注意类名必须为 Main, 不要有任何 package xxx 信息
public class Main {
    public static void main(String[] args) {
        Scanner in = new Scanner(System.in);
        // 注意 hasNext 和 hasNextLine 的区别
        while (in.hasNextLine()) { // 注意 while 处理多个 case
            String line = in.nextLine();

           Set<Character> set = new HashSet<>(128);
            char[] chars = line.toCharArray();
            for (int i = 0; i < chars.length; i++) {
                set.add(chars[i]);
            }

             System.out.println(set.size());
        }
    }
}

通过set方案实现了,而且结果显示也通过了。但如果只是到这里一步,还远远不够,至少还要分析时间复杂度和空间复杂度,判断是否有更优方案

1. 时间复杂度分析

代码外层使用了一次for循环遍历数组,时间复杂度O(N),
内层set.add方法底层使用Map的putVal

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            Node<K,V> e; K k;
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

Map结果在jdk8中采用数组+链表+红黑树结构存储,在putVal时判断tab[i = (n - 1) & hash]是否为null,可以简单理解就是计算valued的hashcode,并通过hashcode值算出在数组的索引位置,看这个索引位置是否有值,若有值,则继续判断value是否相等,若相等则不添加,这个过程是O(1),但若不相等,则会在链化或者树化,链表插入时间复杂度O(1),树插入平均时间复杂度为O(LogN),由于链表超过8个节点后会树化,所以时间复杂度平均为O(LogN)

2. 空间复杂度分析

预估容量O(N),若一个字符是1k,10个字符就是10k。

从上诉两方面分析,SET方案时间复杂度外层循环避免不了,内层中map.putval优化已经非常极致了,空间复杂度是O(N)。一般Set方案已经比较优秀了,但是奈何程序员的智慧是无穷尽的,我们回到问题本身我们只需要统计不重复数据的个数,并不需要知道容器中哪些字符,那容器中存放字符,再去判断数量,感觉有点浪费空间。所以我这里想到了位图方式,统计位图中1的位的数量即可。

方案二:通过Java的BitSet位图实现方案

import java.util.Scanner;
import java.util.*;
// 注意类名必须为 Main, 不要有任何 package xxx 信息
public class Main {
    public static void main(String[] args) {
        Scanner in = new Scanner(System.in);
        while (in.hasNextLine()) { 
            String line = in.nextLine();

           BitSet bitSet = new BitSet(128);
            char[] chars = line.toCharArray();
            for (int i = 0; i < chars.length; i++) {
                if(!bitSet.get(chars[i])){
                   bitSet.set(chars[i]);
                }
            }

            System.out.println(bitSet.cardinality());
        }
    }
}

BitSet源码分析

1. 构造函数分析

 private final static int ADDRESS_BITS_PER_WORD = 6;
 private long[] words;

 public BitSet(int nbits) {
        // nbits can't be negative; size 0 is OK
        if (nbits < 0)
            throw new NegativeArraySizeException("nbits < 0: " + nbits);

        initWords(nbits);
        sizeIsSticky = true;
    }

  private void initWords(int nbits) {
        words = new long[wordIndex(nbits-1) + 1];
    }

 private static int wordIndex(int bitIndex) {
        return bitIndex >> ADDRESS_BITS_PER_WORD;
    }

首先确定bitSet中维护Long类型的words数组,words数组中每个索引位占ADDRESS_BITS_PER_WORD=6位,所以说若words数组数量为1,则可以存放1-63,若数组长度为2,能存放1-4095

上面在构造函数中指定了数据量128,通过initWords方法初始化words
数组的长度,让128-1=127右移6位,高6位补0,获取wordIndex为1

127的二进制->0111 1111
👉右移->6位,高6位补0
0[111 111]1->0[000 000]1

再将wordIndex+1=数组的长度,这样的做法将数据通过位存储,通过2个数组长度就能存储下128个数,大大减少了空间的存储量

2. Set方法和Get方法分析


//set方法
public void set(int bitIndex) {
        if (bitIndex < 0)
            throw new IndexOutOfBoundsException("bitIndex < 0: " + bitIndex);
       //获取数据所在数组索引位
        int wordIndex = wordIndex(bitIndex);
        //扩容
        expandTo(wordIndex);
        //通过或存储值
        words[wordIndex] |= (1L << bitIndex); // Restores invariants
        checkInvariants();
    }

//get方法
  public boolean get(int bitIndex) {
      //不能为负值
        if (bitIndex < 0)
            throw new IndexOutOfBoundsException("bitIndex < 0: " + bitIndex);
      //判断是否越界
        checkInvariants();
      //获取数据所在数组的索引位
        int wordIndex = wordIndex(bitIndex);
        //判断索引位置是否为0
        return (wordIndex < wordsInUse)
            && ((words[wordIndex] & (1L << bitIndex)) != 0);
    }

将get方法和set方法放到一起讲,这里有位运算设计比较巧妙的地方,
set方法中或运算保留word[wordIndex]中原位为1不变,是一个加运算,举例说明

若word[0]中原来存放了一个值2,即0000 0010,
现在word[0]中再放入40000 1000
42进行或运算
2->0000 0010
4->0000 1000
结果->0000 1010 等于6
现在word[0]=6

get方法中的与运算可以获取到原值,以上述word[0]=6来获取4举例

word[0]->0000 1010
      4->0000 1000
与结果为-> 0000 1000 等于4

所以在set中存储words[wordIndex] |= (1L << bitIndex);
get判断是否有值是

return (wordIndex < wordsInUse)
            && ((words[wordIndex] & (1L << bitIndex)) != 0);

上面的wordsInUse变量赋值是在set方法的扩容方法中设置的,代表实际有值的数组长度

private void expandTo(int wordIndex) {
        int wordsRequired = wordIndex+1;
        if (wordsInUse < wordsRequired) {
            ensureCapacity(wordsRequired);
            wordsInUse = wordsRequired;
        }
    
    }

1.时间复杂度

代码外层使用了一次for循环遍历数组,时间复杂度O(N),
内层bitSet.set和get方法底层使用按位与运算(&)和按位或运算(|)的时间复杂度都是常数时间O(1),这是因为它们是在位级别上进行操作,每一位的操作可以在常数时间内完成。
无论操作数的位数多少,按位与运算和按位或运算都只需对每一对对应位进行逻辑运算,不需要依次遍历每一位。这是因为在计算机内部,整数的位表示是以二进制形式存储的,并且在硬件电路层面上,按位与运算和按位或运算可以同时对所有位进行并行操作。

2.空间复杂度

参考博文:两种工具查询Java嵌套对象引用内存大小

public static void main(String[] args) {
        StringBuffer sb = new StringBuffer();
        sb.append("abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz");
        Set<Character> set = new HashSet<>(128);
        BitSet bitSet = new BitSet(128);
        char[] chars = sb.toString().toCharArray();
        for (int i = 0; i < chars.length; i++) {
            set.add(chars[i]);

            if (!bitSet.get(chars[i])) {
                bitSet.set(chars[i]);
            }
        }

        System.out.println("set size is " + set.size());
        System.out.println("bitSet size is " + bitSet.cardinality());
        System.out.println("set memory size is " + RamUsageEstimator.sizeOf(set) + " Bytes");
        System.out.println("bitSet memory size is " + RamUsageEstimator.sizeOf(bitSet) + " Bytes");
    }

对于同一个字符串中,bitset占用空间明显比set占用空间要小很多
在这里插入图片描述

总结

看来每个问题都不只有一种解法,每一种解法优缺点分析需要理解底层的逻辑,并借助工具去呈现最终的结果,只要我们想到并且动手实践,也许也没有那么难

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

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

相关文章

chatgpt赋能python:Python中求最大公约数的方法及实现

Python中求最大公约数的方法及实现 在数学中&#xff0c;最大公约数是指两个或多个整数的共同约数中最大的一个。对于Python开发工程师来说&#xff0c;求最大公约数是一个非常基本的操作&#xff0c;尤其在处理算法或数学题目时更加常见。本篇文章就是为了帮助大家更好的理解…

华为OD机试真题B卷 Java 实现【最长子字符串的长度】

一、题目描述 给你一个字符串s&#xff0c;字符串s首尾相连组成一个环形&#xff0c;请你在环形中找出‘o’字符出现了偶数次最长子字符串的长度。 二、输入描述 输入一串小写字母组成的字符串。 三、输出描述 输出一个整数。 四、解题思路 题目要求在给定的环形字符串中…

kong网关安装及konga安装

一、kong安装 安装机器地址&#xff1a;192.168.19.50 1、自定义一个docker网络 [rootmin ~]# docker network create kong-net a9bde4e7d16e4838992000cd5612476b238f7a88f95a07c994a9f57be7f64c10查看网络是否创建成功 [rootmin ~]# docker network ls NETWORK ID NA…

3DMAX车缝线生成器插件使用方法详解

3dMax车缝线生成器插件,用于创建缝合对象和一个对象,以沿样条线或仅通过绘制选定边上的缝合之间的孔。 目前有两种类型的缝线,圆形缝线和平面缝线。对于给定类型的针脚,它们的厚度是最常用的。缝线的长度和间距以及旋转都可以很容易地调整,这些参数也可以随机设置,以创造…

vscode + CMake 构建C语言项目

文章目录 1. 所需工具2. 配置1. 编写顶级目录下的 CMakeLists.txt2. 编写子目录 src 里的 CMakeLists.txt3. 添加测试文件4. 开始构建 1. 所需工具 Visual Stduio Code&#xff08;vscode&#xff09; CMake 简介&#xff1a; CMake 是一个跨平台的 构建工具&#xff0c;用于 …

[论文阅读73]Prefix-Tuning:Optimizing Continuous Prompts for Generation

1. 基本信息 题目论文作者与单位来源年份Prefix-Tuning&#xff1a;Optimizing Continuous Prompts for GenerationXiang Lisa Li等 Stanford UniversityAnnual Meeting of the Association for Computational Linguistics2021 Citations 1009, References 论文链接&#xf…

基于java的超市管理系统设计与实现

摘 要 随着小型超市规模的发展不断扩大&#xff0c;商品数量急剧增加&#xff0c;有关商品的各种信息量也成倍增长&#xff0c;传统的人工记忆方式也慢慢的无法适应形势的变化。随着信息技术的发展&#xff0c;计算机已被广泛的用于社会的各个领域&#xff0c;成为推动社会发…

EMNLP - 征集系统演示

Call For System Demonstrations - EMNLP 2023 EMNLP 2023 系统演示计划委员会邀请演示计划的提案。演示范围从早期研究原型到成熟的生产就绪系统。特别感兴趣的是公开可用的开源或开放访问系统。鉴于自然语言处理领域的理论和应用研究的现状&#xff0c;我们还强烈鼓励展示技术…

利用画图以及代码分析详细解读外排序的实现过程

外排序的实现 思想代码分析完整代码 如果有海量数据需要排序&#xff0c;而在内存中放不下这么多数据&#xff0c;因此就不能使用内排序&#xff08;直接插入排序&#xff0c;希尔排序&#xff0c;堆排序&#xff0c;快速排序&#xff0c;归并排序等等&#xff09;。关于想了解…

Java利用JOL工具分析对象分布

对象的组成 对象头[Header] Markword&#xff1a;存储对象自身运行时数据如hashcode、gc分代年龄等&#xff0c;64位系统总共占用8个字节&#xff0c;关于Markword详细内存分布如下 类型指针&#xff1a;对象指向类元数据地址的指针&#xff0c;jdk8默认开启指针压缩&#xff…

算法基础学习笔记——⑫最小生成树\二分图\质数\约数

✨博主&#xff1a;命运之光 ✨专栏&#xff1a;算法基础学习 目录 ✨最小生成树 &#x1f353;朴素Prim &#x1f353;Kruskal算法 ✨二分图 &#x1f353;匈牙利算法 ✨质数 &#x1f353;&#xff08;1&#xff09;质数的判定——试除法 &#x1f353;&#xff08;2&…

简单认识OSI(计算机网络分层)七层模型

前言 学校上课讲的太笼统啥也不是&#xff0c;自己学的太玄学似懂非懂突然在看到了一篇公众文文章。文章从初始到现在&#xff0c;步步为营的遇到一个解决一个前人的问题&#xff0c;有了细致入微的讲述&#xff0c;把之前学的死东西都连起来了。 如果让你来设计网络https://m…

chatgpt赋能python:Python取余数:介绍和实际应用

Python取余数&#xff1a;介绍和实际应用 Python是一种高级编程语言&#xff0c;其灵活性和多功能性使其成为开发者的首选之一。在Python中&#xff0c;取余数是常见的数学运算之一&#xff0c;这个操作在编写程序时非常有用。在本文中&#xff0c;我们将介绍Python中的取余数…

chatgpt赋能python:Python中单行输出的使用方法

Python中单行输出的使用方法 Python是广泛使用的高级编程语言之一&#xff0c;具有易于学习、可读性强和简单易用等优点。在Python编程中&#xff0c;我们经常需要输出文本内容&#xff0c;而Python中单行输出便是一个非常重要的功能。 什么是单行输出 单行输出是指将多个元…

TDengine 深入解析缓存技术

TDengine是一款高性能的物联网大数据平台。为了高效处理时序数据&#xff0c;TDengine中大量用到了缓存技术&#xff0c;自己实现了哈希表、缓存池等技术。本文会为大家讲解TDengine中用到的这些缓存技术。 首先会介绍一下什么是缓存&#xff0c;常用的缓存技术&#xff0c;最后…

想知道怎么翻译多个文本?我教你三个好方法吧

随着电子商务的全球化发展&#xff0c;越来越多的企业意识到将产品推向全球市场的重要性。在全球市场中&#xff0c;各种语言和文化的消费者都存在着巨大的潜在需求。为了吸引和服务这些不同语言的客户&#xff0c;企业需要采取一系列的措施&#xff0c;其中翻译是至关重要的一…

科技发展的那些事儿

近30年来&#xff0c;科技发展取得了惊人的成就&#xff0c;涉及范围广泛&#xff0c;包括计算机科学、通讯技术、生物医学、能源等多个领域。本文将列举近30年来科技发展的重要事件&#xff0c;并探讨这些事件对我们的生活、工作和社会产生的影响。 1991年&#xff0c;Linux操…

chatgpt赋能python:Python中可以用八进制表示整数吗?

Python中可以用八进制表示整数吗&#xff1f; Python是一种流行的动态编程语言&#xff0c;它支持许多整数表示方法。八进制是一种表示整数的方法&#xff0c;那么Python中可以使用八进制表示整数吗&#xff1f;本文将探讨这个问题。 什么是八进制&#xff1f; 在计算机科学…

基于SSM的服装设计供需系统设计与实现

摘 要&#xff1a;作为服装设计的重要形式之一&#xff0c;服装具有显著的审美性&#xff0c;是人类情感表达不可忽视的代表形态。但在新时期背景下&#xff0c;随着服装设计的进一步优化&#xff0c;服装设计创新融合强度也随之增强。本文就服装设计供需系统进行深入探究。 服…

chatgpt赋能python:如何在Python中去掉逗号

如何在Python中去掉逗号 在Python编程中&#xff0c;逗号是一个非常常见的符号&#xff0c;它通常用于分隔多个变量或值。然而&#xff0c;有时候我们需要从文本中去掉逗号&#xff0c;以便更好地处理数据。那么在Python中&#xff0c;如何去掉逗号呢&#xff1f;接下来&#…