算法-位图与底层运算逻辑

news2025/1/16 8:00:16

文章目录

    • 1. 位图的理论基础
    • 2. 完整版位图实现
    • 3. 底层的运算逻辑-位运算

1. 位图的理论基础

首先我们要理解什么是位图, 位图的一些作用是什么
位图法就是bitmap的缩写。所谓bitmap,就是用每一位来存放某种状态,适用于大规模数据,但数据状态又不是很多的情况。通常是用来判断某个数据存不存在的。在cpp中的STL中有一个bitset容器,其实就是位图法。其实就是一种为了减少空间而存在存储数字的一种数据结构
其实学习了位图之后, 我更愿意称之为"位映射"
我们基础的位图结构包含下面的几种方法

  1. add方法(向位图中添加该数字)
  2. remove方法(在位图中删除该数字)
  3. reverse/flip方法(在位图中将该位的数字状态进行翻转)
  4. contains方法(检查位图中是否有这个数字)

下面是位图存储原理的分析
在这里插入图片描述

位图可以存储 0 ~ n 范围内的数字, 上面线段表示每一个整数的范围, 一个整数有32个比特位, 所以一个整数可以存储32个整数的状态, 比如第一个整数存储数据的范围是[0,31],第二个是[32,63], 以此类推…, 记住这里面有一个小点就是我们a / b向上取整的代码实现是 (a + b - 1) / b
下面是基础版本位图的实现


/**
 * 位图是一种在指定的连续的范围上替代哈希表来存储数字数据的一种数据结构
 * 构造方法是传入一个数字n, 可以实现从 [0 , n - 1]范围上数字的查询(n个数字)
 * 数字所需的位置在 set[n / 32] , 数字的指定的位数在 n % 32 (从最右侧开始,起始为0)
 */
class BitsSet {
    int[] set;

    public BitsSet(int n) {
        //容量需要进行向上的取整, 可以用数学工具类中的ceiling方法进行向上的取整, 也可以用这个表达式
        // a / b 向上取整的结果是 (a + b - 1) / b
        this.set = new int[(n + 31) / 32];
    }

    /**
     * add方法
     */
    public void add(int n) {
        set[n / 32] = set[n / 32] | (1 << (n % 32));
    }

    /**
     * remove方法(思考为什么用取反不用异或)
     */
    public void remove(int n) {
        set[n / 32] = set[n / 32] & (~(1 << (n % 32)));
    }

    /**
     * reverse/flip方法(如果有这个数字就remove,如果没有就add)
     */
    public void reverse(int n) {
        set[n / 32] = set[n / 32] ^ (1 << (n % 32));
    }

    /**
     * contains方法(检查是不是有这个数字)
     */
    public boolean contains(int n) {
        return ((set[n / 32] >>> (n % 32)) & 1) == 1;
    }
}

2. 完整版位图实现

在这里插入图片描述

上面这个leetcode对位图的完整版的实现, 我们来解析一下这个位图的具体方法, 其中fix方法就是基础版本位图的add方法, unfix其实就是remove方法, flip是一个反转的方法, 可能很多人觉得真的要将位图中的所有的元素的状态都进行翻转, 其实没有必要, 而且如果全部都进行反转是十分消耗资源的, 我们直接采用一种假翻转的状态, 即定义一个布尔类型变量reverse来判断位图的元素是否进行了翻转, 反转之前我们的0代表不存在, 1代表存在, 翻转之后我们的1代表不存在, 0代表存在, 我们基本属性有set(位图的主体), one(1的个数), zero(0的个数), size(元素数量), reverse(是否进行翻转), 这里很有意思, 我们实现的filp方法直接把 reverse = !reverse , zero跟one 的数值交换即可, 就已经实现了我们的交换的目的, 其实这是一种假交换, 代码如下图所示


/**
 * 自己实现一个完整版本的位图
 */
class Bitset {

    //基本的数据集合
    private int[] set;
    //数据的个数
    private int size;
    //1的个数(注意在我们这里不是真实的二进制1的个数)
    private int one;
    //0的个数(注意在我们这里不是真实的二进制0的个数)
    private int zero;
    //判断该bits是否进行了翻转
    private boolean reverse;

    public Bitset(int size) {
        this.size = size;
        set = new int[(size + 31) / 32];
        zero = size;
        one = 0;
        reverse = false;
    }

    public void fix(int idx) {
        int index = idx / 32;
        int bit = idx % 32;
        if (!reverse) {
            //说明没有进行翻转, 此时0表示不存在1表示存在
            if ((set[index] & (1 << bit)) == 0) {
                one++;
                zero--;
                set[index] = set[index] | (1 << bit);
            }
        } else {
            //此时说明已经进行了翻转操作
            if ((set[index] & (1 << bit)) != 0) {
                one++;
                zero--;
                set[index] = set[index] & (~(1 << bit));
            }
        }
    }

    public void unfix(int idx) {
        int index = idx / 32;
        int bit = idx % 32;
        if (!reverse) {
            //此时说明没有发生翻转, 此时0表示不存在1表示存在
            if ((set[index] & (1 << bit)) != 0) {
                one--;
                zero++;
                set[index] = set[index] & (~(1 << bit));
            }
        } else {
            if ((set[index] & (1 << bit)) == 0) {
                one--;
                zero++;
                set[index] = set[index] | (1 << bit);
            }
        }
    }

    public void flip() {
        reverse = !reverse;
        zero = zero ^ one;
        one = zero ^ one;
        zero = zero ^ one;
    }

    public boolean all() {
        return size == one;
    }

    public boolean one() {
        return one > 0;
    }

    public int count() {
        return one;
    }

    //其实就是重写一下toString方法
    @Override
    public String toString() {
        StringBuilder sbd = new StringBuilder();
        int index = 0;
        while (index < size) {
            int num = set[index / 32];
            for (int j = 0; j < 32 && index < size; j++) {
                if (!reverse) {
                    if (((num >>> j) & 1) == 1) {
                        sbd.append(1);
                    } else {
                        sbd.append(0);
                    }
                    index++;
                } else {
                    if (((num >>> j) & 1) == 1) {
                        sbd.append(0);
                    } else {
                        sbd.append(1);
                    }
                    index++;
                }
            }
        }
        return sbd.toString();
    }
}

3. 底层的运算逻辑-位运算

其实计算机的底层实现加减乘除的时候是没有 + - * / 这些符号的区分的, 其实底层的运算的逻辑都是使用位运算拼接出来的…

3.1 加法

首先阐释一下加法的运算的原理就是 加法的结果 = 无进位相加的结果( ^ ) + 进位信息( & 与 << ),当进位信息为 0 的时候, 那个无尽为相加的结果, 也就是异或运算的结果就是答案

在这里插入图片描述

代码实现如下(不懂得看我们位运算的基础中关于异或运算的理解)

//因为是进位信息, 所以获得完了之后要左移一位
public static int add(int a, int b) {
        int ans = a;
        while (b != 0) {
            //ans更新为无尽无进位相加的结果
            ans = a ^ b;
            //b更新为进位信息
            b = (a & b) << 1;
            a = ans;
        }
        return ans;
    }

3.2 减法

减法得实现更加得简答, 就是把我们的 a - b ⇒ a + (-b), 转换为加法进行操作, 代码实现如下

/**
     * 生成一个数相反数的方法
     * 之前我们学过的那个 Brain Kernighan算法中有一个就是 -n == (~n + 1)
     * 所以计算相反数其实就是 add(~n , 1)
     */
    public static int neg(int n) {
        return add(~n, 1);
    }


    /**
     * 减法的运算结果其实就是把 减法转换为加法 比如 a - b = a + (-b);
     */
    public static int sub(int a, int b) {
        return add(a, neg(b));
    }

3.3 乘法

乘法的计算方式本质上是类比的我们小学的时候学习的竖式乘法
也就是说, 乘法的本质实现还是依赖的加法, 代码实现如下

 /**
     * 乘法的计算方式本质上是类比的我们小学的时候学习的竖式乘法
     * 也就是说, 乘法的本质实现还是依赖的加法
     */
    public static int mul(int a, int b) {
        int ans = 0;
        while (b != 0) {
            if ((b & 1) == 1) {
                ans = add(ans, a);
            }
            //这里的b一定要是无符号右移(为了避开负数)
            b = b >>> 1;
            a = a << 1;
        }
        return ans;
    }

3.4 除法

首先介绍得除法的基本逻辑

位运算实现除法(基础的逻辑, 但是不完备)
这个是最特殊的位运算的题目, 因为我们要考虑除数与被除数的正负关系(全部都先转化为正数进行运算)
由于整数的第 31 位是符号位, 所以我们不进行考虑(全部处理为非负), 从30进行考虑
除法的基本逻辑就是 判断一个数里面是否包含 2^i 次方
x >= y * (2^i), 也就是x的i位是1, 反之就是0, 然后让 x - y * (2^i) ; 重复此过程直至判断到最后一位(0位)
所以代码的基本逻辑就是 x >= y << i ; 但是左移可能会溢出, 所以我们改为右移 ==> (x >> i) >= y;
还有一点就是注意这里一定要先把 a b 赋值给 x y 再进行操作, 否则可能会导致后续的 a b 值发生改变影响结果判断
代码实现如下

 public static int div(int a, int b) {
        //先把 a , b 处理为非负的
        int x = a < 0 ? neg(a) : a;
        int y = b < 0 ? neg(b) : b;

        int ans = 0;
        for (int i = 30; i >= 0; i = sub(i, 1)) {
            if ((x >> i) >= y) {
                ans = ans | (1 << i);
                //这一步为什么不会溢出其实我暂时也没懂, 先记下来吧
                x = sub(x, y << i);
            }
        }
        //注意这里的异或运算也可以作用与布尔类型, 其本质就是0 / 1进行的异或运算
        return a < 0 ^ b < 0 ? neg(ans) : ans;
    }

下面的这个才是除法的正确逻辑, 相关注释在代码中体现

下面这个才是相对完备的逻辑, 对一些特殊情况进行了处理, 因为除数与被除数有可能没有相反数(整数最小值越界)

public static final int MIN = Integer.MIN_VALUE;

    public static int divide(int dividend, int divisor) {
        //同时是最小值
        if (dividend == MIN && divisor == MIN) {
            return -1;
        }

        //同时都不是最小值(基本的除法逻辑)
        if (dividend != MIN && divisor != MIN) {
            return div(dividend, divisor);
        }

        //代码执行到这里就说明二者中必有一个是最小值, 另一个不是, 此时需要判断除数是不是-1, 判断会不会越界
        if(divisor == neg(1)){
            return Integer.MAX_VALUE;
        }

        if(divisor == MIN){
            return 0;
        }

        //代码走到这里就只剩下了一种情况, 就是divisor不是-1, dividend是最小值
        //此时你直接运算取反肯定会溢出, 所以此时进行一些变换操作, 此时(a + b)/(a - b)不会溢出
        //1. b为正数 ,  a / b == (a + b - b) / b ==> ((a + b) / b) - 1;
        //2. b为负数 ,  a / b == (a - b + b) / b ==> ((a - b) / b) + 1;
        dividend = add(dividend, divisor < 0 ? neg(divisor) : divisor);
        int ans = div(dividend,divisor);
        int offset = divisor > 0 ? neg(1) : 1;
        return add(ans,offset);
    }

上面除法的一些标准来源于leetcode29计算除法
在这里插入图片描述
谢谢观看

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

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

相关文章

让你500平的大别墅网络信号无死角!一文读懂什么是Mesh

最新的路由器往往拥有超快的速度、多天线和对最新Wi-Fi标准的支持。然而&#xff0c;如果你的设备信号微弱或有死角&#xff0c;再强大的路由器也无济于事。虽然Wi-Fi扩展器是增加网络覆盖范围的廉价替代品&#xff0c;但它们效率低下且带宽较低。 相比之下&#xff0c;Mesh网络…

野外/工地车流计数摄像头,单人即可安装,简单低成本

在野外或工地这样的特殊环境中&#xff0c;对车流进行准确计数对于交通管理、资源调配以及安全保障都具有重要意义。而野外/工地车流计数摄像头的出现&#xff0c;以其单人即可安装、简单低成本的特点&#xff0c;为解决这些场景中的车流统计问题提供了理想的解决方案。 一、野…

【Elasticsearch】一、概述,安装

文章目录 概述全文搜索引擎概述ES&#xff08;7.x&#xff09; 安装ES&#xff08;Docker&#xff09;测试&#xff0c;是否启动成功 可视化工具配置中文 客户端Postman下载 概述 ES是开源的高扩展的分布式全文搜索引擎&#xff0c;实时的存储、检索数据&#xff1b;本身扩展性…

做了个记录心情并结合AI给出建议的小程序

名称&#xff1a;心情记录员 微信小程序名称叫程序记录员&#xff0c;主要功能是记录情绪&#xff0c;然后根据情绪和产生情绪的原因通过AI给出有助于心理健康的建议。同时支持统计功能&#xff0c;可以以查看日历的方式浏览不同日期下情绪的变化轨迹。 主要功能 记录情绪和…

“私域流量:解锁电商新机遇,共创数字化未来“

一、私域流量的战略意义再探 步入数字化浪潮的深处&#xff0c;流量已成为企业成长不可或缺的血液。与广泛但难以掌控的公域流量相比&#xff0c;私域流量以其独特的专属性和复用潜力&#xff0c;为企业铺设了通往深度用户关系的桥梁。它不仅赋能企业实现精准营销&#xff0c;…

应用监控SkyWalking调研

参考&#xff1a; 链路追踪( Skyworking )_skywalking-CSDN博客 企业级监控项目Skywalking详细介绍&#xff0c;来看看呀-CSDN博客 SkyWalking 极简入门 | Apache SkyWalking 使用 SkyWalking 监控 ClickHouse Server | Apache SkyWalking https://zhuanlan.zhihu.com/p/3…

主播一般用什么麦克风?一文看懂哪种麦克风降噪效果好

如今是一个短视频飞速发展的时代&#xff0c;越来越多自媒体人通过短视频的方式来进行直播带货、生活Vlog、线上K歌等&#xff0c;记录下生活里那美丽的瞬间。不过想要拍摄出来的视频作品拥有清晰的音质以及不受环境噪音所影响&#xff0c;那么一款好的麦克风是必不可少的。很多…

Linux驱动开发实战宝典:设备模型、模块编程、I2C/SPI/USB外设精讲

摘要: 本文将带你走进 Linux 驱动开发的世界,从设备驱动模型、内核模块开发基础开始,逐步深入 I2C、SPI、USB 等常用外设的驱动编写,结合实际案例,助你掌握 Linux 驱动开发技能。 关键词: Linux 驱动,设备驱动模型,内核模块,I2C,SPI,USB 一、Linux 设备驱动模型 Li…

如何选择最适合您的短剧系统源码

短剧系统源码是一个帮助用户搭建短剧平台的软件程序。在选择短剧系统源码时&#xff0c;确保选择一个适合您需求的系统是非常重要的。以下是一些关键因素&#xff0c;可以帮助您选择最好的短剧系统源码。 1. 功能丰富性&#xff1a;在选择短剧系统源码之前&#xff0c;您需要明…

ListBox自动滚动并限制显示条数

1、实现功能 限制ListBox显示的最大条数&#xff1b; ListBox自动滚动&#xff0c;显示最新行&#xff1b; 2、C#代码 using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.IO; using Syst…

LED显示屏的尺寸及安装比例设计

在现代商业展示和舞美应用中&#xff0c;LED显示屏已成为不可或缺的视觉设备。为了确保LED显示屏的最佳视觉效果和与环境的完美融合&#xff0c;正确的尺寸及安装比例设计尤为重要。本文将从LED显示屏的安装方法、音响系统、控制系统及配电、质量保证备件等方面&#xff0c;探讨…

Snipaste截图工具的下载

Snipaste是一款简单而强大的桌面截图工具&#xff0c;它不仅支持快速截图&#xff0c;还提供了丰富的编辑和贴图功能&#xff0c;极大地提升了用户的工作效率。 网址&#xff1a;Snipaste 下载 1.进入文件夹解压缩 2.解压缩后打开双击运行 3.快捷键F1截图 F3截图固定桌面 …

日志以及日志封装

日志 输出日志信息 import logging# 调用 指定级别 输入日志信息 logging.debug("this is a debug") logging.info("this is a info") logging.warning("this is a warning") logging.error("this is a error") logging.critical(&qu…

CICD之Git版本管理及基本应用

CICD:持续集成,持续交付--让对应的资料,对应的项目流程更加规范--提高效率 CICD 有很多的工具 GIT就是其中之一 1.版本控制概念与环境搭建 GIT的概念: Git是一款分布式源代码管理工具(版本控制工具) ,一个协同的工具。 Git得其数据更像是一系列微型文件系统的快照。使用Git&am…

Vue 全局状态管理新宠:Pinia实战指南

文章目录 前言全局状态管理基本步骤&#xff1a;pinia 前言 随着Vue.js项目的日益复杂&#xff0c;高效的状态管理变得至关重要。Pinia作为Vue.js官方推荐的新一代状态管理库&#xff0c;以其简洁的API和强大的功能脱颖而出。本文将带您快速上手Pinia&#xff0c;从安装到应用&…

当下环境下如何提升自己以拥抱未来的机会-程序员的自我提升

一、前言 看看今年的行情,无论是国内还是国外,仿佛都没有什么活力,经济下行压力越来越大,企业经营越来越困难。对于程序员的工作机会越来越少。这可能是现阶段乃至几年内的现象。现在是现金为王,拥有其他资产仿佛没有多大的增值空间,经济一片惨淡,消费不活跃,我看到的…

【鸿蒙学习笔记】Column迭代完备

属性含义介绍 Column({ space: 10 }) {Row() {Text(文本描述).size({ width: 80%, height: 60 }).backgroundColor(Color.Red)}.width(90%).height(90).backgroundColor(Color.Yellow) } .width(100%) // 宽度 .height(200) // 高度 .backgroundColor(Color.Pink) // 背景色 .…

汇聚荣拼多多电商如何?

随着互联网技术的飞速发展&#xff0c;电子商务已成为现代商业的重要组成部分。其中&#xff0c;拼多多作为中国领先的电商平台之一&#xff0c;以其独特的社交电商模式迅速崛起&#xff0c;吸引了众多消费者和商家的加入。那么&#xff0c;拼多多电商如何呢?本文将从五个方面…

电路里电源不仅仅是电源

电源往往被认为是直流控制电路中重要的考虑因素之一——但我们也不能忽视其他关键因素&#xff1a;电源滤波器、转换器和备用电源模块。 输入电源是任何电气控制系统的基本配置。没有电源&#xff0c;就没有传感器、控制器、负载设备&#xff0c;什么都没有。因此&#xff0c;…

Three.js 中的光照模型

Three.js 中的光照模型 Three.js 的一个伟大抽象就是统一了所有材质的光照模型, 无论 PBR 或者 Phong。都只用两个函数给全部囊括了。 就是 RE_Direct(直接反射) 和 RE_IndirectDiffuse(间接反射)。真正做到了大一统。下面以Phong为例,具体看一下如何落地。 省流版本: // 直接…