计算机原理&数据结构&算法面试题

计算机原理

冯诺依曼体系结构

现代计算机,大部分都是基于冯诺依曼体系结构

冯诺依曼的核心是:存储程序,顺序执行.

 

冯诺依曼体系结构有以下特点:

1、计算机处理的数据和指令一律用二进制数表示;

2、指令和数据不加区别混合存储在同一个存储器中;

3、顺序执行程序的每一条指令;

4、计算机硬件由运算器、控制器、存储器、输入设备和输出设备五大部分组成。

计算机硬件的介绍

CPU

中央处理器

它是电脑进行运算和控制的核心

以前CPU主要由运算器和控制器两大部分组成

随着集成电路的发展,目前CPU芯片集成了一些其它逻辑功能部件来扩充CPU的功能,如浮点运算器、内存管理单元、cache和MMX等。

 

CPU的基本工作是执行存储的指令序列,即程序。

程序的执行过程实际上是不断地取出指令、分析指令、执行指令的过程。

 

存储器

存储器为CPU提供指令和数据。

电脑中的存储器大致可划分为两大类:一类是主存,即内存;一类是辅存,即外存。

整个存储器系统中包括了寄存器、Cache、内部存储器、外部存储。

层次越高速度越快,但是价格越高,而层次越低,速度越慢,价格越低。

 

寄存器

作用:用来暂时存放指令、数据和地址等。

寄存器是存储器中最快的存在,它往往和CPU同时钟频率。

寄存器是CPU的内部组成单元,但是一个寄存器需要20多个晶体管,所以如果大量使用,CPU的体积会非常大。所以在CPU中只有少量的寄存器。而每个寄存器的大小都是8-64字节。

 

CPU对存储器中的数据进行处理时,往往先把数据取到内部寄存器中,而后再作处理。

 

高速缓存和主存

高速缓存(SRAM)和主存(DRAM)都是RAM

为了解决CPU和主存之间速度不匹配而采用的一项重要技术,它是介于CPU处理器和内存之间的临时数据交换的缓冲区。

L1,L2,L3等多级的高速缓存就是为了满足高性能的cpu,如果内存读写速度足够快就不需要这些高速缓存来做缓冲了,L1到L3越往外,访问时间也就越长,但同时也就越便宜。

高速缓存的大小一般是主存的千分之一左右

高速缓存相比主存速度更快功耗更低,而由于结构相对复杂占用面积较大,所以一般少量在CPU内部用作Cache,而不适合大规模的集成使用(高速缓存存储器集成在CPU上)

 

主存由一个电容和一个晶体管组成的,每一位存储就是对一个电容充电。

 

ROM只读存储

RAM在断电后都会丢失数据,非易失的存储器即便在断点后也能保存数据。一般我们称之为ROM(Read-Only Memory)。虽然这么说,但是ROM在特殊的情况下还是可以写入数据的,否则就不能叫存储器了。

ROM在计算机中应用也比较多,比如我们的BIOS芯片

 

磁盘存储

也就是硬盘。属于外设存储器。磁盘读取操作要比DRAM慢10万倍,比SRAM慢百万倍。

术语说明:

  • 磁盘:磁盘由一层一层的盘片组成,每个盘片区分上下面
  • 磁头:是用于向磁盘读写信息的工具,不同盘面上的磁头是同时移动的。它可以从该面的一个磁道移动到另一个磁道。
  • 磁道:磁盘上的一圈圈的圆周被称之为磁道,靠近主轴的同心圆用于停靠磁头,不存储数据,磁道上凹凸不平,其中凸起的地方代表被磁头划过即数字1,反之,凹的地方表示数字0;
  • 扇区:每圈磁道上的扇形小区域被称为扇区,扇区中存在着很多存储单元用于存储比特信息,扇区是从磁盘读出和写入信息的最小单位,通常大小为512字节
  • 柱面:不同盘面上的每圈磁道所组成的柱形区域,这块区域叫做柱面,一面磁盘上的磁道数=柱面数

 

编号方式:

磁道是从外到内,从0开始编号,即最外面的一圈为第0磁道

扇区的编号方式为固定标记某块为1号,然后顺时针编号

 

磁盘的容量:

磁头数 × 磁道(柱面)数 × 每道扇区数 × 每扇区字节数

磁头(head)数:每个盘片一般有上下两面,分别对应1个磁头,共2个磁头;

 

磁盘读写:

当需要从磁盘读取数据时,系统会将数据逻辑地址传给磁盘,磁盘的控制电路按照寻址逻辑将逻辑地址翻译成物理地址,即确定要读的数据在那个柱面,哪个盘面,哪个扇区。因此读写磁盘上某一数据时需要经过以下步骤:

移动磁头至相关柱面;

确定盘面号后,盘片开始旋转至指定扇区即可。

 

时间计算:

经过以上步骤后,开始读写数据。其操作时间可以表示为:

          寻道时间+盘片旋转时间+数据传输时间

寻道时间:磁头移动定位到指定磁道,跟磁头上次所在位置有关,最大可达到0.1s左右

盘片旋转时间:等待盘片的指定扇区旋转至磁头下盘片旋转一般为7200转/分,即一圈需要约8ms;

数据传输时间:通过系统总线传输一般为0.02us/B,即20ms/MB。

 

系统从磁盘读取数据到内存时是以磁盘块(block)为基本单位的,位于同一个磁盘块中的数据会被一次性读取出来,而不是需要什么取什么。

 

 

读写原理:

磁盘即是磁表面存储器,也就是在不同形状上(如盘状,带状等)的载体上涂有磁性材料层,工作时,靠载磁体高速运动,由磁头在磁层上进行读/写操作,信息被记录在磁层上,这些信息的轨迹也就是磁道,磁盘的磁道是一个个同心圆

写入内容时,记录介质在磁头下方匀速通过,根据运行情况对写入线圈输入一定方向和大小的电流,使磁头导磁体磁化,产生一定方向和强度的磁场。由于磁头与磁表面间距非常小,磁力线直接穿透磁层表面,将对应磁头下方的微小区域磁化(称为磁化单元),可以根据写入驱动电流的不同方向,使磁层表面被磁化的极性方向不同,以区别记录0或1:

 

相对于CPU,内部存储的电子结构,磁盘存储是一种机械结构。数据都通过电磁流来改变极性的方式被电磁流写到磁盘上,而通过相反的方式读回。一个硬盘由多个盘片组成,每个盘片被划分为磁道,扇区和最小的单位簇。而每个盘面都有一个磁头用来读取和写入数据。而硬盘的马达装置则控制了磁头的运动。

 

机械硬盘和固态硬盘

机械硬盘又叫HHD;固态硬盘又叫SSD

HHD的数据传输速率要比SSD要慢很多倍,而且在抗摔性和外观大小上也是SSD更加好

固态硬盘在容量和价格上是远远比不上机械硬盘的

主要由主控与闪存芯片组成的SSD可以以更快速度和准确性访问驱动器到任何位置。

总线

总线是各种功能部件之间传送信息的公共通信干线,它是由导线组成的传输线束。

总线的宽度指的是总线可同时传输的数据数,以比特为单位,总线宽度愈大,传输性能就愈佳。

总线的带宽指的是单位时间(每秒)内总线上传送的数据量

总线的位宽指的是总线能同时传送的二进制数据的位数,或数据总线的位数,即32位、64位等。

按照计算机所传输的信息种类,计算机的总线可以划分为

数据总线:它既可以把CPU的数据传送到存储器或输入输出接口等其它部件,也可以将其它部件的数据传送到CPU。

地址总线:专门用来传送地址的,由于地址只能从CPU传向外部存储器或I/O端口,所以地址总线总是单向的

控制总线:控制总线主要用来传送控制信号和时序信号。控制总线的传送方向由具体控制信号而定,一般是双向的

 

总线也可以按照CPU内外来分类:

  • 内部总线:在CPU内部,寄存器之间和算术逻辑部件ALU与控制部件之间传输数据所用的总线称为片内部总线。
  • 外部总线:通常所说的总线指片外部总线,是CPU与内存RAM、ROM和输入/输出设备接口之间进行通讯的通路,也称系统总线。

输入输出设备

输入设备:键盘,鼠标,麦克风

输出设备:显示器,打印机,音响和耳机

 

计算机存储

进率为什么是1024

目前计算机都是二进制的,让它们计算单位,只有2的整数幂时才能非常方便计算机计算

 

为什么使用二进制

因为电脑内部的电路工作有高电平和低电平两种状态.所以就用二进制来表示

 

存储的基本单元

字节Byte是存储器存储信息的最小单位

 

 

操作系统原理

虚拟内存

虚拟内存是计算机系统内存管理的一种技术。别称虚拟存储器

 

它使得应用程序认为它拥有连续的可用的内存(一个连续完整的地址空间),而实际上,它通常是被分隔成多个物理内存碎片,还有部分暂时存储在外部磁盘存储器上,在需要时进行数据交换。目前,大多数操作系统都使用了虚拟内存

 

比如:每个进程创建加载的时候,会被分配一个大小为4G的连续的虚拟地址空间,虚拟的意思就是,其实这个地址空间时不存在的,仅仅是每个进程“认为”自己拥有4G的内存,而实际上,它用了多少空间,操作系统就在磁盘上划出多少空间给它,等到进程真正运行的时候,需要某些数据并且数据不在物理内存中,才会触发缺页异常,进行数据拷贝

 

虚拟内存别称虚拟存储器。电脑中所运行的程序均需经由内存执行,若执行的程序占用内存很大或很多,则会导致内存消耗殆尽。为解决该问题,Windows中运用了虚拟内存技术,即匀出一部分硬盘空间来充当内存使用。当内存耗尽时,电脑就会自动调用硬盘来充当内存,以缓解内存的紧张。

 

host文件介绍

host文件说明

其作用就是将一些常用的网址域名与其对应的IP地址建立一个关联“数据库”,当用户在浏览器中输入一个需要登录的网址时,系统会首先自动从Hosts文件中寻找对应的IP地址,一旦找到,系统会立即打开对应网页 +,如果没有找到,则系统会再将网址提交DNS域名解析服务器进行IP地址的解析。

 

位置:

window系统:

C:\Windows\System32\drivers\etc目录下的hosts文件

linux系统:

/etc下面的hosts文件

 

数据结构

数据结构的定义

数据结构就是对数据对象中数据元素关系的描述

数据结构种类

集合(元素间为松散的关),线性结构(一对一关系),树形结构(一对多关系),图状结构(多对多关系)

常用的数据结构

数组(Array):

数组是一种聚合数据类型,它是将具有相同类型的若干变量有序地组织在一起的集合

 

栈( Stack)

栈是一种特殊的线性表,它只能在一个表的一个固定端进行数据结点的插入和删除操作。数据后进先出

 

队列(Queue)

队列是一种特殊的线性表,队列只允许在表的一端进行插入操作,而在另一端进行删除操作。数据先进先出

 

链表( Linked List)

链表是一种数据元素按照链式存储结构进行存储的数据结构,这种存储结构具有在物理上存在非连续的特点。链表由一系列数据结点构成,每个数据结点包括数据域和指针域两部分。

 

树( Tree)

一种简单的非线性结构。有

 

图(Graph)

堆(Heap)

散列表(Hash):

哈希表的存储过程如下:

根据 key 计算出它的哈希值 h。

假设数组的长度为 n,那么这个键值对应该放在第 (h % n) 个数组位置中。

如果该位置中已经有了键值对,就使用开放寻址法或者拉链法(链表法)解决冲突。

(哈希表的数据结构就是:数组或数组跟其他数据结构的组合如链表)

开放寻址法是对冲突之后查询数组上有空位的地方放入数据,拉链法就是在冲突位置上使用链表存储多个值

线性结构特点

特点:有且只有一个根结点;表中的每一个数据元素,除了第一个以外,有且只有一个前件。除了最后一个以外有且只有一个后件。可以表示为(a1,a2,……an)

 

线性结构的存储结构

线性表的两种存储结构分别是顺序存储结构和链式存储结构。

常见的线性结构:线性表、栈、队列、一维数组、串

 

常见的:二叉树

图状结构(或网状结构)

 

一维数组与队列对比

  • 数组是静态数据结构创建时就确定了大小,队列是动态数据结构创建后还可以增加或减小其大小
  • 数组可以通过索引直接访问任何位置的元素,队列只能在队列的两端进行操作:从队尾插入元素,从队头删除元素。
  • 队列可以使用数组实现,也可以使用链表实现

队列和栈对比

栈(stack)的特点是后进先出(入栈push,出栈pop,遍历traverse)

队列的特点是先进先出

 

链表

链表是一种物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。

链表由一系列结点(链表中每一个元素称为结点)组成。每个结点包括两个部分:一个是存储数据元素的数据域,另一个是存储下一个结点地址的指针域。

 

链表的优点:

元素的个数可以自由扩充 、插入、删除等操作不必移动数据,只需 修改链接指针

缺点:

访问数据需要遍历空间开销大

 

头结点:

第一个结点之前附设一个结点,它没有直接前驱,称之为头结点。数据域可以不存储任何信息(有时候也会存放链表的长度等等信息),它指向表中第一个结点。

头结点的作用是使所有链表(包括空表)的头指针非空,并使对单链表的插入、删除操作不需要区分是否为空表或是否在第一个位置进行,从而与其他位置的插入、删除操作一致。使算法变的简单

如果当链表为空的时候,头结点的指针域的数值为NULL.

 

从链表的实现方式可以把链表分为单链表,循环链表,双向链表。

单链表:

1)每个节点只包含一个指针,即后继指针。

2)尾节点的后继指针指向空地址null。

3)性能特点:插入和删除节点的时间复杂度为O(1),查找的时间复杂度为O(n)。

 

循环链表

1)除了尾节点的后继指针指向头节点的地址外均与单链表一致。

2)适用于存储有循环特点的数据

 

双向链表

1)节点除了存储数据外,还有两个指针分别指向前一个节点地址(前驱指针prev)和下一个节点地址(后继指针next)。

2)首节点的前驱指针prev和尾节点的后继指针均指向空地址。

3)性能特点:

和单链表相比,存储相同的数据,需要消耗更多的存储空间。

对于一个有序链表,双向链表的按值查询效率要比单链表高一些。只需要查找一半的数据

数组和链表对比

数组申请的是一块连续的内存空间,数据存储的逻辑顺序跟物理上内存的顺序是一致的,已知第一个元素首地址和每个元素所占字节数,则可求出任一个元素首地址(也就是可以随机存取)。创建的时候就确定了空间大小,数组中插入、删除数据项时,需要移动其它数据项,非常繁琐

 

链表在物理上非连续的内存空间,动态地进行存储分配,现用现申请,添加和删除数据方面,只需要知道操作位置的指针,很方便可以实现增删

数组可以随机读取,链表要找一个数,必须要从头开始找起(遍历)

同时链表由于增加了结点的指针域,空间开销比较大。

 

(如果数组的中间插入一个元素,那么这个元素后的所有元素的内存地址都要往后移动.删除的话同理.只有对数据的最后一个元素进行插入删除操作时,才比较快.链表只需要更改有必要更改的节点内的节点信息就够了.并不需要更改节点的内存地址

 

 

树形结构

树是用来表示层次关系,因表示的样子很像一颗倒立的树而得名。

根结点:每个结点只有一个前件,称为父结点,没有前件的结点只有一个,称为树的根结点。

子结点:每一个结点可以有多个后件结点,称为该结点的子结点。没有后件的结点称为叶子结点

结点度:一个结点所拥有的后件个数称为树的结点度

树的深度或高度:树的最大层次称为树的深度或高度。

树的种类

二叉树

二叉树是每个结点最多有两个子树的树结构。通常子树被称作“左子树”和“右子树”

二叉树常被用于实现二叉查找树和二叉堆

二叉树的每个结点至多只有二棵子树(不存在度大于2的结点)

满二叉树:除最后一层以外,每一层上的所有结点都有两个子结点。

完全二叉树:除最后一层以外,每一层上的结点数均达到最大值;在最后一层上只缺少右边的若干结点。

二叉查找树

定义:

又称为是二叉排序树(Binary Sort Tree)或二叉搜索树。

具有下列性质的二叉树:

  1. 1) 若左子树不空,则左子树上所有结点的值均小于它的根结点的值;
  2. 2) 若右子树不空,则右子树上所有结点的值均大于或等于它的根结点的值;
  3. 3) 左、右子树也分别为二叉排序树;
  4. 4) 没有键值相等的节点。

平衡二叉树

平衡二叉树又被称为AVL树(有别于AVL算法),且具有以下性质:它是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树,同时,平衡二叉树必定是二叉搜索树,反之则不一定。

 最小二叉平衡树的节点的公式如下 F(n)=F(n-1)+F(n-2)+1

 

红黑树

定义:

红黑树是一种自平衡二叉查找树

 

红黑树是每个节点都带有颜色属性的二叉查找树,颜色为红色或黑色。

 

性质:

性质1. 节点是红色或黑色。

性质2. 根是黑色。

性质3. 所有叶子都是黑色(叶子是NIL节点)。

性质4. 每个红色节点必须有两个黑色的子节点。(从每个叶子到根的所有路径上不能有两个连续的红色节点。)

性质5. 从任一节点到其每个叶子的所有简单路径都包含相同数目的黑色节点。

这些约束确保了红黑树的关键特性: 从根到叶子的最长的可能路径不多于最短的可能路径的两倍长

 

时间复杂度:

能保证在最坏情况下,在O(logn)时间内做查找,插入和删除,这里的n是树中元素的数目。

 

常见类对象的数据结构

String:

char型数组存储

ArrayList:

Object型数组

LinkedList:

双向链表,元素在Node中存储

hashMap:

数组+链表+红黑树

 

 

 

 

 

自己实现hashMap

public class MyHashMap {
    //默认初始化大小 16
    private static final int DEFAULT_INITIAL_CAPACITY = 16;
    //默认负载因子 0.75
    private static final float DEFAULT_LOAD_FACTOR = 0.75f;
    //临界值
    private int threshold;
    //元素个数
    private int size;
    //扩容次数
    private int resize;
    private HashEntry[] table;
    public MyHashMap() {
        table = new HashEntry[DEFAULT_INITIAL_CAPACITY];
        threshold = (int) (DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR);
        size = 0;
    }
    private int index(Object key) {
        //根据key的hashcode和table长度取模计算key在table中的位置
        return key.hashCode() % table.length;
    }
    public void put(Object key, Object value) {
        //key为null时需要特殊处理,为简化实现忽略null值
        if (key == null) return;
        int index = index(key);
        //遍历index位置的entry,若找到重复key则更新对应entry的值,然后返回
        HashEntry entry = table[index];
        while (entry != null) {
            if (entry.getKey().hashCode() == key.hashCode() && (entry.getKey() == key || entry.getKey().equals(key))) {
                entry.setValue(value);
                return;
            }
            entry = entry.getNext();
        }
        //若index位置没有entry或者未找到重复的key,则将新key添加到table的index位置
        add(index, key, value);
    }
    private void add(int index, Object key, Object value) {
        //将新的entry放到table的index位置第一个,若原来有值则以链表形式存放
        HashEntry entry = new HashEntry(key, value, table[index]);
        table[index] = entry;
        //判断size是否达到临界值,若已达到则进行扩容,将table的capacicy翻倍
        if (size++ >= threshold) {
            resize(table.length * 2);
        }
    }
    private void resize(int capacity) {
        if (capacity <= table.length) return;
        HashEntry[] newTable = new HashEntry[capacity];
        //遍历原table,将每个entry都重新计算hash放入newTable中
        for (int i = 0; i < table.length; i++) {
            HashEntry old = table[i];
            while (old != null) {
                HashEntry next = old.getNext();
                int index = index(old.getKey());
                old.setNext(newTable[index]);
                newTable[index] = old;
                old = next;
            }
        }
        //用newTable替table
        table = newTable;
        //修改临界值
        threshold = (int) (table.length * DEFAULT_LOAD_FACTOR);
        resize++;
    }
    public Object get(Object key) {
        //这里简化处理,忽略null值
        if (key == null) return null;
        HashEntry entry = getEntry(key);
        return entry == null ? null : entry.getValue();
    }
    public HashEntry getEntry(Object key) {
        HashEntry entry = table[index(key)];
        while (entry != null) {
            if (entry.getKey().hashCode() == key.hashCode() && (entry.getKey() == key || entry.getKey().equals(key))) {
                return entry;
            }
            entry = entry.getNext();
        }
        return null;
    }
    public void remove(Object key) {
        if (key == null) return;
        int index = index(key);
        HashEntry pre = null;
        HashEntry entry = table[index];
        while (entry != null) {
            if (entry.getKey().hashCode() == key.hashCode() && (entry.getKey() == key || entry.getKey().equals(key))) {
                if (pre == null) table[index] = entry.getNext();
                else pre.setNext(entry.getNext());
                //如果成功找到并删除,修改size
                size--;
                return;
            }
            pre = entry;
            entry = entry.getNext();
        }
    }
    public boolean containsKey(Object key) {
        if (key == null) return false;
        return getEntry(key) != null;
    }
    public int size() {
        return this.size;
    }
    public void clear() {
        for (int i = 0; i < table.length; i++) {
            table[i] = null;
        }
        this.size = 0;
    }
    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder();
        sb.append(String.format("size:%s capacity:%s resize:%s\n\n", size, table.length, resize));
        for (HashEntry entry : table) {
            while (entry != null) {
                sb.append(entry.getKey() + ":" + entry.getValue() + "\n");
                entry = entry.getNext();
            }
        }
        return sb.toString();
    }
}
class HashEntry {
    private final Object key;
    private Object value;
    private HashEntry next;
    public HashEntry(Object key, Object value, HashEntry next) {
        this.key = key;
        this.value = value;
        this.next = next;
    }
    public Object getKey() {
        return key;
    }
    public Object getValue() {
        return value;
    }
    public void setValue(Object value) {
        this.value = value;
    }
    public HashEntry getNext() {
        return next;
    }
    public void setNext(HashEntry next) {
        this.next = next;
    }
}

 

posted @ 2023-02-02 10:23  星光闪闪  阅读(333)  评论(0)    收藏  举报