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

news/2024/11/29 17:27:58/

题目: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 处理多个 caseString 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 1sttreeifyBin(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 keyV 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 OKif (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 invariantscheckInvariants();}//get方法public boolean get(int bitIndex) {//不能为负值if (bitIndex < 0)throw new IndexOutOfBoundsException("bitIndex < 0: " + bitIndex);//判断是否越界checkInvariants();//获取数据所在数组的索引位int wordIndex = wordIndex(bitIndex);//判断索引位置是否为0return (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 10104->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.ppmy.cn/news/104883.html

相关文章

夜深人静学32系列16——RTC实时时钟

RTC时钟 RTC什么是RTC&#xff1f;RTC结构框图CubeMX配置RTC代码配置 实战——简易时钟任务要求代码实现实验结果 补充唤醒功能配置代码如下&#xff1a; RTC 什么是RTC&#xff1f; RTC(Real Time Clock)&#xff1a;实时时钟 RTC是个独立的定时器。RTC模块拥有一个连续计数…

如何使用Keras选择添加的层并搭建神经网络?

目录 1.神经网络搭建步骤2.常见的层详解及其用途3.常见神经网络4.代码示例 1.神经网络搭建步骤 选择神经网络的各层需要根据具体问题和数据集的特点进行调整&#xff0c;一般可以通过以下步骤来进行&#xff1a; 1.确定输入数据的维度和特征数量&#xff0c;这有助于确定网络…

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

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

小主机折腾记12 HP 285G3 PRO MT

五一期间&#xff0c;无事&#xff0c;咸鱼购入HP 285G3 PRO MT折腾 HP 285 PRO G3 MT准系统 150包邮 R5 2600 250包邮 金百达3200内存 229京东包邮 直接说准系统情况&#xff1a; 1.主机有三个sata接口&#xff0c;两个硬盘接口&#xff0c;一个光驱接口&#xff08;应该是可以…

web 前端技术体系大全(IT枫斗者)

web 前端技术体系大全 前端技术框架 Vue.js 官网&#xff1a;https://cn.vuejs.org/Vue CLI&#xff1a;https://cli.vuejs.org/菜鸟教程&#xff1a;http://www.runoob.com/w3cnote…Nuxt.js&#xff1a;https://zh.nuxtjs.org/桌面应用 Electron&#xff1a;https://elect…

2. JVM内存模型深度剖析与优化

JVM性能调优 1. JDK的体系结构2. Java语言的跨平台特性3.JVM整体结构及内存模型3.1 内存模型3.1.1 PC寄存器&#xff08;线程私有&#xff09;3.1.2 虚拟机栈&#xff08;线程私有&#xff09;1. 局部变量表2. 操作数栈 本文是按照自己的理解进行笔记总结&#xff0c;如有不正确…

数据结构与算法06:递归和简单的排序

目录 【递归】 【排序】 冒泡排序 插入排序 选择排序 【每日一练&#xff1a;K 个一组翻转链表】 【递归】 递归是将一些有规律的重复问题分解为同类的子问题的方法&#xff0c;也就是在函数中自己调用自己。比较经典的递归代码就是 斐波那契数列&#xff0c;实现方式如…

算法02-入门算法枚举与模拟算法

文章目录 总结大纲要求枚举算法枚举思想枚举举例题目描述 统计因数题目描述 质数判定错误方法一&#xff1a;优化方法1&#xff1a; 用break实现优化优化方法2&#xff1a; sqrt(n) 题目描述 水仙花数题目描述 7744问题实现方法1优化方法2 题目描述 余数相同问题题目描述 特殊自…