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

文章目录

    • 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.mfbz.cn/a/764562.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【HDC.2024】探索无限可能:华为云区块链+X,创新融合新篇章

6月23日&#xff0c;华为开发者大会2024&#xff08;HDC 2024&#xff09;期间&#xff0c; “「区块链X」多元行业场景下的创新应用”分论坛在东莞松山湖举行&#xff0c;区块链技术再次成为焦点。本次论坛以"区块链X"为主题&#xff0c;集结了行业专家、技术领袖、…

fyne的MultiLineEntry设置大小

MultiLineEntry设置大小 在另一篇文章讲过&#xff0c;放入border布局中&#xff0c;可以最大化MultiLineEntry。 这里再介绍另一种方法:SetMinRowsVisible() func (e *Entry) SetMinRowsVisible(count int) {e.multiLineRows counte.Refresh() }SetMinRowsVisible强制mult…

Typora(跨平台 Markdown 编辑器 )正版值得购买吗

Typora 是一款桌面 Markdown 编辑器&#xff0c;作为国人开发的优秀软件&#xff0c;一直深受用户的喜爱。 实时预览格式 Typora 是一款适配 Windows / macOS / Linux 平台的 Markdown 编辑器&#xff0c;编辑实时预览标记格式&#xff0c;所见即所得&#xff0c;轻巧而强大…

Linux kernel 与 设备树

Linux kernel 与 设备树 1 介绍1.1 概述1.2 发展历程1.3 各版本发布时间及特色1.4 Linux 单内核1.5 Linux 内核网址1.6 NXP 官方镜像与 野火 鲁班猫镜像的区别 2 Linux 内核组成2.1 进程管理2.2 内存管理2.3 文件系统2.4 设备管理2.5 网络功能 3 Linux 内核编译3.1 编译 Kernel…

llm学习-2(使用embedding和数据处理)

首先可以简单了解一下向量数据库相关知识&#xff1a; 向量数据库相关知识&#xff08;搬运学习&#xff0c;建议还是看原文&#xff0c;这个只是我自己的学习记录&#xff09;-CSDN博客 补充&#xff1a; 使用embedding API 文心千帆API Embedding-V1是基于百度文心大模型…

【STM32】GPIO复用和映射

1.什么叫管脚复用 STM32F4有很多的内置外设&#xff0c;这些外设的外部引脚都是与GPIO复用的。也就是说&#xff0c;一个GPIO如果可以复用为内置外设的功能引脚&#xff0c;那么当这个GPIO作为内置外设使用的时候&#xff0c;就叫做复用。 STM32F4系列微控制器IO引脚通过一个…

我使用 GPT-4o 帮我挑西瓜

在 5 月 15 日&#xff0c;OpenAI 旗下的大模型 GPT-4o 已经发布&#xff0c;那时网络上已经传开&#xff0c; 但很多小伙伴始终没有看到 GPT-4o 的体验选项。 在周五的时候&#xff0c;我组建的 ChatGPT 交流群的伙伴已经发现了 GPT-4o 这个选项了&#xff0c;是在没有充值升…

仓库管理系统25--数据导出

原创不易&#xff0c;打字不易&#xff0c;截图不易&#xff0c;多多点赞&#xff0c;送人玫瑰&#xff0c;留有余香&#xff0c;财务自由明日实现 1、添加用户控件 <UserControl x:Class"West.StoreMgr.View.DataExportView"xmlns"http://schemas.microsof…

《单片机》期末考试复习-学习笔记总结

题型 问答题(15分)编程题(65分)编程题1(20分)编程题2(45分)设计题(20分)一、问答题 1.1.单片机概念和特点 1.2. 51单片机的中断结构 1.3.主从式多机通讯的概念及其工作原理 多机通信是指两台以上计算机之间的数据传输,主从式多机通信是多机通信系统中最简单的一种,…

springboot个体快餐订单系统-计算机毕业设计源码13441

目 录 摘要 1 绪论 1.1 研究背景 1.2研究意义 1.3论文结构与章节安排 2 个体快餐订单系统系统分析 2.1 可行性分析 2.2 系统流程分析 2.2.1 数据流程 3.3.2 业务流程 2.3 系统功能分析 2.3.1 功能性分析 2.3.2 非功能性分析 2.4 系统用例分析 2.5本章小结 3 个…

MySQL常用操作命令大全

文章目录 一、连接与断开数据库1.1 连接数据库1.2 选择数据库1.3 断开数据库 二、数据库操作2.1 创建数据库2.2 查看数据库列表2.3 删除数据库 三、表操作3.1 创建表3.2 查看表结构3.3 修改表结构3.3.1 添加列3.3.2 删除列3.3.3 修改列数据类型 3.4 删除表 四、数据操作4.1 插入…

Java UU跑腿同城跑腿小程序源码快递代取帮买帮送源码小程序+H5+公众号跑腿系统

&#x1f680;【同城生活小助手】&#x1f680; &#x1f3c3;‍♂️【同城跑腿&#xff0c;即刻送达的便利生活】&#x1f3c3;‍♀️ 在快节奏的都市生活中&#xff0c;时间成了最宝贵的资源。UU跑腿小程序&#xff0c;作为同城生活的得力助手&#xff0c;让你轻松解决生活…

校园卡手机卡怎么注销?

校园手机卡的注销流程可以根据不同的运营商和具体情况有所不同&#xff0c;但一般来说&#xff0c;以下是注销校园手机卡的几种常见方式&#xff0c;我将以分点的方式详细解释&#xff1a; 一、线上注销&#xff08;通过手机APP或官方网站&#xff09; 下载并打开对应运营商的…

百度AI使用-图像文字识别

前言 百度AI接口可以免费试用&#xff0c;本文描述如何申请使用该资源&#xff0c;以及在QT-Demo下使用百度AI接口&#xff0c;实现图像文字识别功能。 一、百度AI资源申请使用 1.浏览器访问&#xff1a;https://apis.baidu.com&#xff0c; 注册百度智能云账号 2.可以购买试…

AI的价值——不再那么“废”人,保险行业用AI人员流失减少20%

最近有个热点挺让人唏嘘的&#xff0c;某咖啡店员工对顾客泼咖啡粉&#xff0c;我们对于这个事件不予评价。但是对员工这种情绪崩溃&#xff0c;我们所接触的行业中也有不少例子&#xff0c;比如保险行业&#xff0c;相信大家经常会被打保险电话&#xff0c;这类电话很容易就被…

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

Image Image($r(app.media.zhibo)).width(96) // 图片宽度.height(96) // 图片高度.borderRadius(12) // 图片圆曲度.objectFit(ImageFit.Fill) // 不明objectFit Column({ space: 20 }) {Row() {Image($r(app.media.startIcon)).width(66).height(66).borderRadius(12)}.bac…

软考高级之系统分析师及系统架构设计师备考过程记录

0x00 前言 考了两次系分&#xff0c;一次架构&#xff0c;今年系分终于上岸。 在此记录备考过程和一些体会 先说我自己的情况&#xff0c;我本硕都是计算机科班出身&#xff0c;本科的时候搞Java后端开发&#xff0c;硕士搞python和深度学习&#xff0c;但工作之后就基本没碰过…

工业实时操作系统对比:鸿道Intewell跟rt-linux有啥区别

Intewell和RT-Linux是两种不同的实时操作系统&#xff08;RTOS&#xff09;&#xff0c;它们具有各自独特的特点和优势。以下是Intewell操作系统的一些关键特性&#xff0c;以及与RT-Linux的比较&#xff1a; 自主研发&#xff1a;Intewell是由科东软件自主研发的工业嵌入式实…

【基于R语言群体遗传学】-3-计算等位基因频率

书接上文&#xff0c;我们讲完了哈代温伯格基因型频率&#xff0c;也使用数据进行了拟合&#xff0c;那么接下来就是考虑一些计算的问题&#xff1a; 【基于R语言群体遗传学】-1-哈代温伯格基因型比例-CSDN博客 【基于R语言群体遗传学】-2-模拟基因型&#xff08;simulating …

PyGithub,一个超酷的 Python 库!

更多资料获取 &#x1f4da; 个人网站&#xff1a;ipengtao.com 大家好&#xff0c;今天为大家分享一个超酷的 Python 库 - pygithub。 Github地址&#xff1a;https://github.com/pygithub/pygithub GitHub 是目前最流行的代码托管平台之一&#xff0c;提供了丰富的API接口来…