权衡时空-理解存储
本章重点理解计算机的存储,解决和存储有关的问题,就必须要了解计算机的存储结构,以及不同存储设备的的特点。在任何时候,如果想要成为一个好的工程师,都需要了解计算机的存储结构。与存储相关的理论和技术大致可以被分为两个角度来讨论:
- 围绕数据的使用特点和使用设备展开->比如数据是随机访问还是顺序访问,需要一次获取大量数据还是获取单个数据。
- 围绕存储系统本身的体系机构特点展开。从信息角度看,计算机的本质是传输、处理、存储信息的机器,传输、处理可以被视为是一个直进直出结果,但是存储信息则是一个经过多层次的过程。
例题7.1
给定一个32位或者64位的二进制数,如何有效的数出其中1的数量?(MS、AB)
解法1:AB公司给出的x&(x-1)方法。假设 x = x 1 , x 2 ⋯ x n x=x_1,x_2\cdots x_n x=x1,x2⋯xn,最右边的1是 x i x_i xi,因此 x = x 1 , x 2 ⋯ x i − 1 , 1 ⋯ 0 , 0 , ⋯ 0 x=x_1,x_2\cdots x_{i-1},1\cdots 0,0,\cdots 0 x=x1,x2⋯xi−1,1⋯0,0,⋯0,并且x-1= x = x 1 , x 2 ⋯ x i − 1 , 0 ⋯ 1 , 1 , ⋯ 1 x=x_1,x_2\cdots x_{i-1},0\cdots 1,1,\cdots 1 x=x1,x2⋯xi−1,0⋯1,1,⋯1。于是,x&(x-1)= x = x 1 , x 2 ⋯ x i − 1 , 0 ⋯ 0 , 0 , ⋯ 0 x=x_1,x_2\cdots x_{i-1},0\cdots 0,0,\cdots 0 x=x1,x2⋯xi−1,0⋯0,0,⋯0,其中的1比x少了一个。当x&(x-1)=0时,说明x中不包含1了。
int count = 0;
while(x&(x-1) != 0){
count ++;
x = x&(x-1);
}
解法2:将上述很长的“二进制数”以8位一个单元分成几部分。8位二进制数只能表示256种可能性,所以将每一种可能性对应的1的数量记录下来。在这个数组中,i[0]=0,i[1]=1,i[2]=1,i[3]=2…i[255]=8。对于任何一个二进制数只查一次表就可以了。
255的二进制为:1111 1111。
->PS:如果不考虑空间成本,分组K越大,时间不一定越短。因为计算机的高速缓存区是有限的,在真实情况下,处理器不是直接从内存中读取数据,而日是从处理器和内存的之间的高速缓存区(cache)读取数据,高速缓存区容量很小,存不下大的数组。
访问:顺序OR随机
顺序存储和随机存储,看到这两个概念的对比,我就想起来两种基本的数据结构:数组和链表(最近在复习数据结构)。本质上来说,数组和链表都属于线性表。malloc一个堆空间后,数组中所有的元素肯定都是从首地址出发开始,以4个字节连续存储每一个元素(以int形为例)。相反,链表在存储过程中不是连续的,而是通过指针将他们串联起来,所以看起来是连续的,实际存储则是靠指针将分散存储的数据连接起来的。这是我对顺序和随机的一点理解,下面结合例题,总结一下吴老师的观点。
例题7.2 高频单词的二元组问题:
如何用一台服务器从海量文本中(语料库)中,比如1TB的数据中,统计频率最高的100万个单词二元组
负责任的说,低可行性现阶段不具备可行性。
三种低可行性解决方法:
-
第一种方法是直接定义一个二维的大数组来存储一个二元组的出现次数。
举一个例子:比如字典中有200000个单词,我们就可以编号1,2,3,4···200000,矩阵的规模是200000$\times$200000;题目中的一句话为“如何用一台服务器…”,“如何”对应8080、“用”对应19023,那么“如何-用”这个二元组在矩阵中的存放位置就是第8080行、第19023列。遍历过程中,如果再次遇到这个二元组,那么结果就自动加1。这种做法是最直观地做法,但是忽略了计算机的存储能力根本就没有这么大。200000个单词,两个单词在一起的组合就是400亿个,一个二元组需要四个字节,所以一共就是1600亿个字节,即160GB。而今天服务器的存储量远远小于这个这个数。
-
第二种方法是利用第四章编码的知识,对建立的二元组进行状态压缩,将大规模的二维矩阵通过减少索引列,转换为稀疏矩阵。但是在现实世界中,二维矩阵是非常稀疏的,也就是说,非零元素极少。所以利用稀疏矩阵也并不会节省很多的内存空间。而且,利用稀疏矩阵要每一行或者每一列的去统计非零元素的个数。但是统计非零元素之前我们并不知道每一行中具体有多少个非零元素的个数,一旦稀疏矩阵的预留空间被全部填满,就要和一维数组一样,整体的移动插入数据,非常消耗空间和时间。所以,本质上这种方法和第一种方法区别也就不大了。
-
第三种方法是建立一个哈希表。“如何-用”这个二元组作为键,顺序扫描整个文本,不断更新值的情况。1TB的数据,扫描完存起来大约需要1/4~1/2TB的存储空间。->齐普夫定律:在自然语言语料库中一个单词出现的频率与它在频率表里的排名成反比。频率排名第一的词是排名第二的词出现频率的2倍,是排名第一百名的100倍。上述方法缺少可行性,而不是方法本身错误。
两种可行性解决方法:
- 统计分布规律
- 分治
层次:容量VS速度
计算机存储器的体系结构
概念剖析:
主频:时钟频率,就是处理器每秒状态的改变的频率,通常改变一次完成一个计算步骤。主频的倒数被称为是一个时钟周期。
第一级高速缓存区L1的容量,一般用来存储指令,一般用来存储数据。
命中缓存:计算式使用的数据恰好在L1中。
缓存未命中:计算式使用的数据不在在L1中。
一旦缓存未命中,处理器就要适用第二级高速缓存区L2,从L2读取数据时,会同时把L2中的一部分内容复制到L1中。如果L2也没命中,因特尔的处理器还有第三级高速缓存区L3。
三级高速缓存都未能命中,那就只好到内存中去寻找数据了。但是CPU和内存之间有一点很长的物理电路,所以一旦开始访问内存,处理速度就会大打折扣。
索引:地址VS内容
索引的建立与访问效率。
哈希表容易实现随机数据的访问,但是很难找到所查询数据的内容实现顺序访问。而有序索引则相反,容易实现顺序访问,但是如果访问一个数据,组需要进行折半查找。
章节思考题解答
思考题2.2
Q1.二叉树遍历的伪代码
先序遍历
void DepthFirstTraverseTree(BiTree tree){
if(tree == NULL) return;
PrintNode();
DepthFirstTraverseTree(tree->left);
DepthFirstTraverseTree(tree->right);
}
中序遍历
void DepthFirstTraverseTree(BiTree tree){
if(tree == NULL) return;
DepthFirstTraverseTree(tree->left);
PrintNode();
DepthFirstTraverseTree(tree->right);
}
后序遍历
void DepthFirstTraverseTree(BiTree tree){
if(tree == NULL) return;
DepthFirstTraverseTree(tree->left);
DepthFirstTraverseTree(tree->right);
PrintNode();
}
Q2.创建二叉排序树并输出排序结果
题给数组:arr[] = { 5,2,8,0,10,7,18,20,30,12,15,1 }
#include<stdio.h>
#include<stdlib.h>
//二叉排序树的建立
//建立规则:所有左子树的值都小于root->val,所有右子树的值都大于root->val
//递归调用后,左右子树也是二叉排序树
typedef int Elemtype;
//属性结构的创建
typedef struct BSTNode {
Elemtype root;
struct BSTNode* left;
struct BSTNode* right;
}BSTNode,*BiTree;
//插入函数
int Insert_BST(BiTree& T, Elemtype k) {
//base case根节点操作
if (T == NULL) {
//为T申请新的地址空间
T = (BiTree)malloc(sizeof(BSTNode));
T->root = k;
T->left = T->right = NULL;
return 1;
}
else if (T->root == k) {
//有相同的元素不能插入
return 0;
}
//递归插入新的节点
else if (T->root < k) {//设置了C++的引用
return Insert_BST(T->right, k);//函数调用完成后,左孩子会和父节点自动关联起来
}
else if (T->root > k) {
return Insert_BST(T->left, k);
}
}
//创建二叉排序树
void Creat_BST(BiTree &T,Elemtype arr[],int n) {
T = NULL;
int i = 0;
while (i < n) {
Insert_BST(T, arr[i]);
i++;
}
}
//中序遍历
void InOrder(BiTree p) {
if (p != NULL) {
InOrder(p->left);
//putchar(p->root);
printf("%d\t", p->root);
InOrder(p->right);
}
}
//主函数
int main() {
BiTree root = NULL;//树根
Elemtype arr[] = { 5,2,8,0,10,7,18,20,30,12,15,1 };
Creat_BST(root, arr, 12);
printf("the sorted of the result is:\n");
InOrder(root);
printf("\n");
return 0;
}
对于二叉排序树,中序遍历一次即可得到排序最终结果。
封面照片引:微博:@铁憨憨nangesfg——《2098》大国朋克系列;本人最爱的画集之一