【Review】计算机硬件、Cache;数组算法【二分、双指针】

算法训练


数组专题


关于微服务和分布式的介绍博主会抽时间继续分享,要说将所有的知识记住是不可能的,但是有印象 + 知道位置 【那么就不是学新技术,简单一看就立马记住】、接下来一段时间来点中规中矩的计算机基础知识【操作系统、网络、计组】和算法的专题,这些内容统一放在interviewer专栏中

关于java的数组的基础知识先不过多介绍

Part 1 :

以后每天的博客就是这样子的了【专项巩固计划 ---- 耗时1 month 以上】,maybe 你会觉得无聊,但是you may find something interesting;

计算机系统基础

这里的内容可能十分浅显,深入部分以后会分析

计算机系统是由硬件系统和软件系统组成的,二者相互协作

计算机硬件系统

计算机硬件系统通俗来说就是我们可以看见的部分,【台式机】,那么就主机和外设, 主机包括CPU和内存储器; 外设包括输入输出设备、外存、网络通信设备

冯诺依曼计算机五大组件: 运算器、控制器、存储器、输入设备、输出设备;

在这里插入图片描述

这里和上面有一些区别: 外部存储器【硬盘】按照五大组件来看输属于存储器; 但是按照组成原理来说属于外部设备;因为CPU要通过内存向硬盘中写入数据,如果没有向硬盘中持久化数据,拔掉硬盘,程序还是可以运行【外部设备】; 这个就没有绝对的归属;它首先就是一个存储器,然后对于计算机来说属于外设

CPU【central Processing Unit】中央处理单元

CPU基本组成结构示意图 的图像结果

(process n.过程 v.处理), CPU是计算机的核心,负责指令相关操作;

CPU由控制器和运算器以及相关的寄存器组CRS、总线组成

运算器

运算器功能:

  • 执行所有的算数运算和附加运算
  • 执行所有的逻辑运算和测试 【&& ! ||】

组成:

注意运算器包括很多部分,包括ALU(不要觉得ALU就是运算器:happy:)算术逻辑单元arithmetic and logic unit ;还有累加寄存器(AC)、数据缓冲寄存器(DR)、状态条件寄存器(PSW)

  • ALU : 处理数据,算数逻辑运算 — 运算的
  • AC 累加器accumulate : 工作区, ALU操作时提供一个 【 相当于运算的工作台,内存读取的数据的存放位置】工作区— 原操作数在其中,并且暂存运算结果
  • DR data: 数据缓冲寄存器: 缓冲, 因为CPU和存储器之间的操作的速度不同,内存读写的数据或者指令就先放到缓冲区等待指令执行完
  • PSW program state: 状态条件寄存器: 存放状态条件; 也就是ALU运算结果的状态,比如N、I、D【负数、中断,方向】 — I中断异常记录
控制器

功能:

计算机运行过程的自动化,保证程序正确执行,还要处理异常【程序控制、操作控制、时间控制】 — 比如顺序、怎么操作(指令)、执行时间呀; 控制器的核心就是控制、指令

组成:

  • 指令寄存器IR : instruction register,存放指令的,
  • 程序计数器PC: program counter 存放要执行的下一条指令的地址 自动会修改 + 1 【IR是内容,PC是地址】—很好理解,一种引用罢了
  • 地址寄存器 AR: address register 当前CPU访问的内存单元的地址【功能和运算器部件的DR数据缓冲…类似】因为速度的差异,需要AR暂时保存,直到当前指令完成【是内存的地址】PC是指令的地址
  • 指令译码器ID : instruction decoder 指令包括操作码和操作数; ID就是对操作码进行分析解释,识别之后发出具体控制信号 【操作码之前提过的,就四位的比如1111】

指令: 对机器进行程序控制的最小单位: 一条指令包括操作码和操作数【操作数可能是操作数本身或者是操作数的地址】比如存放在内存的数据,或者是数据本身 【操作码由ID翻译】

寄存器组:

  • 专用寄存器: 运算器中的寄存器AC、DR、PSW、控制器中的IR、AR作用固定,就是专用寄存器
  • 通用寄存器CR: 用途不固定,可以指定,处理器不同就不同,不同的CPU
进制的表达: 二进制B、八进制O、十进制D、十六进制H;进制转换有两种方式MSB、LSB都可

存储器的概念不是很复杂: 主要就是内外存,还有就是不同的材料制作,常用的就是磁盘、光盘; 还有对于读写进行限制;  还有就是寻址方式可能有差异: 随机存储器Randon Access Memory --- 因为随机,所以访问时间同; 顺序SAM --- 访问的时间和位置有关【磁带顺序播放】; 直接存储DRM --- 【磁道寻址随机,但是一个磁道是顺序】 ---- 一个磁道就是一个盘片上的一个同心圆
磁盘的性能就是读取的数据的速率【存取速度快才是高性能】,所以首先就考虑寻道时间、磁盘转速;--- 读取快,盘片数和磁道数都是决定容量的,没有速度 -- 和磁盘性能无关   【一般磁盘的容量都是差不多的,容量大但是如果太慢没用】
总线bus

总线就是主要就是在两个硬件之间要传输信息使用;通信链路

  • 数据总线DB data bus : 传输数据信息,双向【cpu到内存/IO; 内存到CPU】,DB宽度决定数据交换的大小
  • 地址总线 AB: 传输Cpu放出的地址信息, 单向【CPU到内存,内存不需要给地址CPU】,宽度决定寻址能力
  • 控制总线CB: 传送控制信号、时序、状态信息,每一条线的单向确定,但是整体可能都是
输入输出技术I/O *

IO设备有很多类型,CPU从I/O设备读取信息,也需要地址总线,控制总线传送过来一个读取的信号,还有就是传递读取的位置(外存还是内存)

程序的控制方式: 无条件、程序查询、中断方式都需要CPU的参与,CPU是稀缺资源,并且万一外设挂了,不好处理

  • 无条件传送: 外设总是准备好,无条件随时接收和提供CPU的数据
  • 程序查询方式: CPU通过程序查询外设的状态,准备好再传数据【比如查询鼠标是否可用】
  • 中断方式: CPU不等待,CPU不主动了,CPU可以专心做自己的操作,外设发送请求,向CPU发送中断请求,CPU中断操作来查询
    • 发出中断信号中断CPU的工作,中断向量表法【中断向量表用来保存中断源的中断服务程序的入口地址(也就是之前的操作的现场)】 中断控制器来进行控制
    • CPU的中断响应时间: 关键就是响应,那就是收到之后到做出反应的时间【发出中断请求到开始进入中断处理程序】
  • DMA方式 直接内存存取: 数据传输再主存和外设之间直接进行,不需要CPU干预,DMA的硬件部件直接完成;DMAC控制器直接控制内存和外设通过IO接口交互
    • DMA方式传送数据当然需要总线的参与,整个系统总线完全交给了DMAC,CPU不用,所以会占用总线周期
  • 通道方式和外围处理机方式IOP: 增加硬件,进一步提高效率

计算机体系结构

计算机体系结构就是就是计算机的概念性结构按照处理机的数量:

  • 单处理系统uniprocessing system: 一个处理单元
  • 并行处理与多处理系统: 两个以上的处理机
  • 分布式处理系统distributed processing system 物理上的远距离、松耦合

安徽走微观上的并行程度

Flynn 、冯泽云、Handler、Kuck 分类法

CISC和RISC

ISA指令集体系结构:一个处理器支持的指令和指令的字节级编码,一个程序编译在一个机器上,在另外的可能不能运行,大端机、小端机

  • CISC: complex instruction set Computer 复杂指令集计算机,进一步增强原有指令的功能 【 这样instruction庞大杂乱】
  • RISC reduced instruction set computer 精简指令集计算机 减少指令总数、简化指令功能,主要就是优化

二者一个扩张,一个精简优化

流水线技术

流水线技术的意思就是将指令操作分解为一条流水线执行【 指令执行任务包括: 取指令 分析指令 执行指令】,交给几个工人一直流水线操作,

  • 流水线周期: 各子任务执行时间最长的子任务的执行时间 (比如执行指令时间最慢,那么周期就是它的时间)— 木桶效应 【并行技术相比串行确实简化了很多】
  • 流水线执行n条指令需要的时间T = 一条指令执行所需要的完整的时间 + (n - 1) * 流水线周期 (第一条指令是完整的执行,但是后面的是前面的还没有执行完就在执行了)
比如这里执行取指令1ns 分析指令2ns, 执行指令3ns

流水线周期就是最慢的就是3ns   执行100条指令

串行方式的时间: 100* 6 = 600
并行 : 6 + 99 * 3 = 303
效率提升了一倍  【流水线就是前一条执行的时候就可以把这条指令的快的部分执行了】
  • 实际吞吐率: 对于指令,就是单位时间执行的指令条数 N/T

  • 最大吞吐率: 单位时间里流水线处理及流出的结果数,

p = 1/ max(t1,t2…) 就是流水线周期的倒数

存储系统

存储器系统顶层是CPU寄存器,其速度和CPU的速度相当;第二层是高速缓冲存储器,CPU速度接近;第三层是主存储器,RAM,内存(随机寻址);第四层是磁盘,最后一层是光盘、磁带【SAM】,越向上,速度越快,容量越小,单位的价格越高【性能】

高速缓存cache

Cache是介于CPU和主存之间的一级存储,其容量很小,速度快,比内存(主存)快

  • 作用: 调试CPU速度和内存速度的差异,从而提升系统性能; 缓冲的DR,AR;
  • 原理: 程序局部性原理: 访问过的数据可能会再次访问,访问过的数据的周围的数据可能马上访问; 所以cache就是局部主存的副本
  • 操作过程: 当CPU需要读取数据的时候,首先判断访问的信息是否在Cache中,如果在就是命中,不再就是要按照替换算法将主存的一块信息放入Cache中
  • 替换算法: 随机替换算法、先进先出替换算法、近期最少使用替换算法、优化替换算法
  • 地址映像: CPU工作时给出的时贮存的地址,Cache中式副本,要从Cache中读取信息,就需要将主存中地址转换成Cache存储器的地址 — 映像
直接映像

主存的块与Cache块的对应关系是固定的,主存的块只能存放在Cache存储器的相同块号中; 也就是说主存的0号块只能存储在cache的0号块中

  • 地址变换简单,访问的速度快
  • 块冲突率高,Cache空间得不到充分利用(因为块号必须对应)
主存地址
   主存区号   区内块号   块内地址  (和现在的住房类似,单元、栋、户)

主存容量1MB,Cache容量16KB,块的大小512B; 那16k/512 就是分为32块;主存就先分区,一个区的大小就是一个Cache的大小;1M/16k = 64个区

在这里插入图片描述

通过这个图就可以看出冲突的概率极大,所有的第0块的数据就不能同时放在Cache中;于此同时,只能放在固定的块中,Cache很多地方没有使用

全相联

全相联就是主存和Cache都均分成容量相同的块;和上面相同,块大小相同,但是关系不固定, 允许主存的任何一块可以调入存储器的任何一块的空间中

  • 灵活,块冲突率第,只有Cache的块全部装满,才可能出现冲突,Cache利用率高
  • 无法通过主存块号直接获得所对应的Cache的块号,变化复杂,成本高【对应关系不明显,只能记录】
主存地址
   主存块号,块内地址   【不分区了】,就一直从0到最后

在这里插入图片描述

组相联

前两种方式的折中;就是先将Cache中的块再分成组,组采用直接映像,但是块还是全相联

主存的任何区的0组只能存到cache的0组,但是组内的块可以任意存

主存地址位数 = 区号 + 组号 + 主存块号 + 块内地址
Cache地址位数 = 组号 + 组内块号 + 块内地址 

主存 : 区 【和cache一样大小】   组   块  地址

Cache的性能分析:

设Hc为Cache的命中率(在Cache中),Tc为Cache的存取时间,Tm为主存的访问时间,那么等效访问时间就是

T = HcTc + (1-Hc)Tm   //很好理解,就是分布律
  • 虚拟存储器实际上是一种逻辑存储器 【也就是利用MMU memory management unit来将虚拟的逻辑地址转化为真实的物理地址】 — 比如编号0x0001; 对应硬盘中的某一块确定的物理地址
  • 相联存储器是一种按照内容访问的存储器 ; 相联【就是产生联系,上面的Cache和主存就是相联,两个的地址有一定的对应关系,这里的相联存储器是按照内容进行访问,和散列表类似,根据内容就直到存储的位置】

接下来关于其他的存储和安全模型tomorrow继续🎄

Part 2

算法题和数学题一样,长时间不做就手生了,所以要多做几次,回忆起来

二分查找 边界的新思考

704. 二分查找 - 力扣(LeetCode) (leetcode-cn.com)

class Solution {
    public int search(int[] nums, int target) {
        int left = 0, right = nums.length;
        int mid = 0;
        while(right > left) {
            mid = left + (right - left)/2;
            if(nums[mid] > target) {
                right = mid;
            }
            else if(nums[mid] < target) {
                left = mid + 1; //好久不写手生了,忘记排除了,死循环
            }
            else {
                return mid;
            }
        }
        return -1;
    }
}

二分查找就是不断分成两份,比较中间值,所以复杂度就是O(logN),之前做二分查找, 总是疑惑边界的问题,还有< 和 <=的关系 【这个各位务必要分清楚,不然就会超时,因为出不了循环】,博主将近4个月没写过算法了,果然easy的又犯错了; 其实这里= 和右边界right有关,这里选的是length,但是我们最后一个元素是num -1; 说明 是【 ); 那么既然是开,就不能=了;并且判断了的元素就要排除,所以对right开就是直接=;对于左边界就是-1

二分法就是关键由right决定,要么是【】,要么【)

还有一道完全类似的搜索题【如果要求空间复杂度,并且数组有序,就要考虑二分查找法】

35. 搜索插入位置 - 力扣(LeetCode) (leetcode-cn.com)

这里一开始应该想到的是暴力,就一个for循环,然后依次和target比较就行,但是O(N); 要求O(logN),一想到有序,就二分法啦!🎉

class Solution {
    public int searchInsert(int[] nums, int target) {
        int temp = 0;
       for(int i = 0; i < nums.length; i ++) { //暴力解法考虑左右边界
           if(nums[i] == target) {//先判断了是否相等,才判断大于,
               temp = i;
           }else if(nums[i] < target) {
               temp = i + 1;
           }
       }
       return temp;
    }
}

//

这道题和上面的二分查找唯一的不同点就是没有找到就要返回该插入的位置

class Solution {
    public int searchInsert(int[] nums, int target) {
        int left = 0, right = nums.length;
        while(left < right) {
            int mid = left + (right - left)/2;
            if(nums[mid] > target) {
                right = mid;
            } else if(nums[mid] < target) {
                left = mid + 1;
            } else {
                return mid;
            }
           //最后就是二者相等的退出循环
        }
        return left;
    }
}

这里唯一需要想清楚的就是最后的left和right相等了,并且就是需要插入的位置【同时注意循环中不要写错了,不要太急了,根据区间的开闭来决定】

69. x 的平方根 - 力扣(LeetCode) (leetcode-cn.com)

这里的暴力解法会溢出,因为数字很大,所以不采用

class Solution {
    public int mySqrt(int x) {
        //二分求根,非负整数,所以直接不断查找,先暴力
        if(x == 1) return 1;
        for(int i = 0; i < x; i ++) {//考虑边界,这里可能1
            if((long)i*i <= x && (long)(i + 1) * (i + 1) > x) {
                return i;
            }
        }
        return 0;
    }
}

那么就二分查找

class Solution {
    public int mySqrt(int x) {
        //二分求根,非负整数,x*x 会溢出,所以使用m/x
       int left = 0, right = x;
       while(right >= left) {
           int mid = left + (right - left)/2;
           if((long)mid*mid > x ) {
               right = mid - 1;
           }else {
               left = mid + 1;
           }
       }
       return right;
    }
}  //这里用除法不安全,可抛出异常

367. 有效的完全平方数 - 力扣(LeetCode) (leetcode-cn.com)

这个题,用暴力完全不行,会超时;所以就二分,因为也是一个有序的序列嘛

class Solution {
    public boolean isPerfectSquare(int num) {
        int left = 0, right = num;
        while(right >= left) {
            int mid = left + (right - left)/2;
            long temp = (long)mid*mid;
            if(temp == num){//上一个题是小于等于,就是找左边界,边界问题就是合并一个即可
                return true;
            }else if (temp > num) {
                right = mid - 1;
            }else {
                left = mid + 1;
            }
        }
        return false;
    }
}
各位写的时候要仔细,博主将left写成num了,找了好一会

二分查找边界【有序但有重复】

2089. 找出数组排序后的目标下标 - 力扣(LeetCode) (leetcode-cn.com)

这个题目是一个简单题,这个题目第一思路还是从最简单的开始,一个排序 + 一个for循环即可; 排序可以先简单排序O(n2 + n),也就是O(n2)

//可以直接使用类库Array.sort()
class Solution {
    public List<Integer> targetIndices(int[] nums, int target) {
        //排序
        /**
        	数组长度为n,则1 <= n < 47 使用插入排序
			数组长度为n,则47 <= 47 < 286 使用使用快速排序
			数组长度为n,则286 < n 使用归并排序或快速排序(有一定顺序使用归并排序,毫无顺序使用快排)
       */
        Array.sort(nums);
        List<Integer> list = new ArrayList();
        //然后进行查找[这个时候就是查找左右边界了]
        for(int i = 0; i < nums.length; i++) {
            if(nums[i] == target) {
                list.add(i);
            }
        }
    return list;
    }
}

其实这里如果不考虑前面的排序,可以将O(N)的复杂度讲到O(logN), 这里排序后不就是有序的数组🐎; 所以就可以使用变化的二分查找算法【二叉树】

其实查找边界真的没有好大的变化,查找边界就是要进行收缩,比如查找左边界,那么就对不断向左收缩,普通的二分就是找到之后记录,现在就是找到之后记录,当作没有找到,继续向左查找

class Solution {
    public List<Integer> targetIndices(int[] nums, int target) {
       Arrays.sort(nums);
       List<Integer> list = new ArrayList();
        //排序后使用二分法找左右边界【just for test】
        int left = 0, right = nums.length;
        int begin = -1;
        while(left < right) {
            int mid = left + (right - left)/2;
            if(nums[mid] >= target) {
                right = mid;
                if(nums[mid] == target)
                    begin = mid;
            }else{
                left = mid + 1;
            }
        }
        //右边界,start和end重新赋值
        left = 0;
        right = nums.length;
        int end = -1;
         while(left < right) {
            int mid = left + (right - left)/2;
            if(nums[mid] <= target) {
                left = mid + 1;
                if(nums[mid] == target)
                    end = mid;
            }else{
                right = mid;
            }
        }
        //插入数组
        if(begin == -1) {
            return list;
        }
        for(int i = begin; i <= end; i++) {
            list.add(i);
        }
        return list;
    }
}

但是思考一下就知道,完全没有必要排序:happy:,只要知道比其大或者小的数目即可

class Solution {
    public List<Integer> targetIndices(int[] nums, int target) {
        int less = 0, more = 0;
       for(int i = 0 ; i < nums.length; i ++) {
            if(nums[i] > target) {
                more ++;
            }else if(nums[i] < target) {
                less ++;
            }
       }
    
       List<Integer> list = new ArrayList();
       for(int i = less; i < nums.length - more; i ++){
           list.add(i);
       }
       return list;
    }
}

这里的复杂度一下子就降到O(N)

总结一下二分【当数组有序递增的时候,都要考虑二分】,关键是边界,最开始定义的开闭;查找过程中排除找过的元素; 查找边界就是找到了记录但是还是要继续收缩,当作没有找到

双指针法删除重复元素

27. 移除元素 - 力扣(LeetCode) (leetcode-cn.com)

双指针主要目的是降低空间复杂度,比如移除元素,常规思路一定是使用一个辅助数组来考虑,但是这里就是将自身考虑,相当于自身有两个index,一个指向待操作的源,一个指向已经操作的新

class Solution {
    public int removeElement(int[] nums, int val) {
        //想复杂了,就一个数组自复制而已,想到了太多api了
        //自复制就是不是变量的时候才进行复制,相当于就是两个本身进行复制; 这里的算法就是双指针法,两个index
        int t = 0;
        for(int i = 0 ; i < nums.length; i ++) {
            if(nums[i] != val) {
                nums[t++] = nums[i];
            }
        }
        return t;
    }
}

这里的i就是源数组的index;t就是自身新的index;双指针

但是看了看题解,发现还是可以优化,这里双指针都是通向行驶,最坏情况需要O(N); 那么其实可以将一个index移到末尾,这样就是想向行驶,复杂度最坏O(N/2)

class Solution {
    public int removeElement(int[] nums, int val) {
        //相向的就是之前我最开始的思路,将相同的移到后面区去;但是弄复杂了,遍历了一次,可以直接While的
       int end = nums.length - 1;
       int left = 0;
       while(end >= left) {
           //当时一直在想如果后面相等怎么做,现在发现就是就算发现了,也不应该增加,只有不等才增加,所以不使用for循环,用while;for循环不好控制
           if(nums[left] == val) {
               nums[left] = nums[end];
               end -- ; //赋值之后递减
           }else{
               left ++; //不相等才增加,所以如果还相等,会多执行一次
           }
       }
       return left;
    }
}

最开始博主就是想的这种方式,但是用for循环根本就越弄越复杂,害,所以要有素养;边界条件还是很easy的

这里的题是一样的

26. 删除有序数组中的重复项 - 力扣(LeetCode) (leetcode-cn.com)

这种自反的数组都是类型都用双指针来表示即可,这里和上面几乎没有区别

class Solution {
    public int removeDuplicates(int[] nums) {
        int k = 1; //第一个没有
        int temp = 0; //记录元素的
         temp = nums[0];
        for(int i = 0; i < nums.length; i ++) {
            if(nums[i] != temp) {
                temp = nums[i];
                nums[k++] = nums[i];
            }
        }
        return k;
    }
}

双指针都是对数组的元素进行操作,可以用于去重或者去除某种元素,特点就是空间复杂度小,自身和自身操作

283. 移动零 - 力扣(LeetCode) (leetcode-cn.com)

class Solution {
    public void moveZeroes(int[] nums) {
        int k = 0;
        for(int i = 0; i < nums.length; i ++) {
            if(nums[i] != 0) {
                nums[k++] = nums[i];
            }
        }
        for(int i = nums.length - 1 ; i >= k; i --) {
            nums[i] = 0;
        }
    }
}

这里就很简单了,但是现在的缺陷就是做题慢,准备多练习一下,提升速度🚀

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值