计算机是怎样跑起来的(三)

30 阅读13分钟

计算机是怎样跑起来的

和算法成为好朋友的七个要点

要点1:算法中解决问题的步骤是明确且有限的

算法的定义为“被明确定义的有限个规则的集合,用于根据有限的步骤解决问题。”用通俗易懂的语言来说就是,“把解决文图的步骤无一遗漏地用文字或图表示出来”。要是把这里的“用文字或图表示”替换为“用编程语言表达”,算法就变成了程序。而且必须注意一个条件,就是“步骤必须是明确的并且步骤数必须是有限的”。

要点2:计算机不靠直觉而是机械式的解决问题

计算机不能自发地思考。因此计算机所执行的由程序表示的算法必须是由机械的步骤所构成。所谓“机械的步骤”,就是不动用任何脑筋,只要按照这个步骤就一定能完成的意思。

要点3:了解并应用典型算法

作为程序员的修养,我们最低限度应该了解一些典型算法。

名称用途
辗转相除法求解最大公约数
埃拉托斯特尼筛法判定素数
顺序查找检索数据
二分查找检索数据
哈希查找检索数据
冒泡排序数据排序
快速排序数据排序

要点4:利用计算机的处理速度

无论多么冗长繁琐的步骤,只要明确并且机械就能构成优秀的算法。把算法用程序表示出来让计算机去执行,而计算机会用令人吃惊的数度为我们执行。在思考算法时不妨时刻记着,解决问题时可以利用计算机的处理速度。

要点5:使用编程技巧提升程序执行速度

解决一个问题的算法未必只有一种。在考量用于解决同一个问题的多种算法的优劣时,可以认为转化为程序后,执行时间较短的算法更为优秀。虽然计算机的处理速度快得惊人,但当处理的数据数值巨大或是数量繁多时还是需要花费大量的时间。

在算法技巧中有个著名的技巧叫做“哨兵”。这个技巧多用在线性搜索(从若干个数据中查找目标数据)等算法中。线性搜索的基本过程是将若干个数据从头到尾,依次逐个对比,直到找到目标数据。

要点6:找出数字间的规律

所有的信息都可以用数字表示-这是计算机的特性之一。因此为了构造算法,经常会利用到存在于数字间的规律。

构造算法时需要找出数字间的规律不仅适用于数字游戏,编写用于计算工资的应用程序时,计算工资的规则也可以说是一种数字上的规律。如果能够发现“工资=底薪+加班补贴+交通补贴-预扣税款”这样的规律,那么解决问题的步骤就是明确的,步骤数也是有限的,因此构造出的算法也就是优秀的了。

与数据结构成为好朋友的七个要点

要点1:了解内存和变量关系

计算机所处理的数据都存储在了被称为内存的IC中。在一般的个人计算机中,内存内部被分割成了若干个数据存储单元,每个单元可存储8比特数据(8比特=1字节)。为了区分各个单元,每个单元都被分配了一个编号,这个编号被称为“地址”就是“门牌号码”。如果一个个人计算机装配有64M字节的内存,那么就会有从0到64M(M=100万)这么多个地址。

因为依靠指定地址的方式编写程序很麻烦,所以在C语言、Java、BASIC等几乎所有的编程语言中,都是使用变量把数据存储进内存,或从内存中把数据读出来。如下C语言写的一段程序,用于把数据“123”存入变量a中。

char a;  /** 定义变量 */
a = 123; /** 把数据存入变量 */

对于程序员来说,他们并不需要知道变量a被存到内存空间中的哪个地址上了。因为当程序运行时是由操作系统为我们从尚未使用的内存空间中划分出一部分分配给变量a的。变量是程序中数据存储的最小单位,每个变量都对应着一块物理上的内存空间

要点2:了解作为数据结构基础的数组

在实际应用的程序中经常需要处理大量的数据,比如那种用于统计1000名职员的工资之类的程序。在这类程序中存储数据时使用的是“数组”,而不是定义出1000个变量以供使用。通过使用数组,既可以同时定义出多个变量,又可以提高编写程序的效率。

数组实际上是为了存储多个数据而在内存上集中分配出的一块内存空间,并且为这块空间整体赋予了一个名字。

数组是数据结构的基础,之所以这么说是因为数组反应了内存的物理结构本身。在内存中存储数据的空间是连续分布的。而在程序中,往往要从内存整体中分配出一块连续的空间以供使用。如果用程序中的语句表示这种分配使用方式的话,就要用到数组。

要点3:了解数组的应用——作为典型算法的数据结构

数组是数据结构的基础,只要使用数组就能通过程序实现各种各样的算法以处理大量的数据。

要点4:了解并且掌握典型数据结构的类型和概念

数组是一种直接利用内存物理结构的最基本的数据结构。只需使用for语句,就可以连续地处理数组中所储存的数据,实现各种各样的算法。但是在现实世界中也有一些数据结构,仅凭借数据是无法实现的,比如有的数据结构可以把数据堆积得像小山一样,有的数据结构可以把数据排成一队,有的数据结构可以任意地改变数据的顺序,有的数据结构可以把数据分成两路排列,等等。为了用程序实现这些数据结构,就必须设法改造数组,但是与之相应的内存的物理结构又是改变不了的。这该怎么办才好?

就像在算法中有典型算法一样,在数据结构中也有典型数据结构,它们是由老一辈程序员发明创造的。这些数据结构都是通过程序从逻辑上改变了内存的物理结构,即数据在内存上呈现出连续分布状态。

名称数据结构的特征
把数据堆积得像小山一样
队列把数据排成一队
链表可以任意地改变数据的顺序
二叉树把数据分成两路排列

“栈”存取数据的方式称为LIFO(Last In Fist Out,后进先出),即最后被存入的数据最先被处理。

“队列”存取数据的方式称为FIFO(Fist In Fist Out,先进先出),即最先被存入的数据也是最先被处理的。

“链表”的概念就相当于几个人手拉着手排成一排。某个人只要松开拉住的那只手,再去拉住另外一只手,这一排人(相当于数据)的顺序就改变了。而且只要先松开拉住的手,再让一个新人加入进来并拉住他的手,就相当于完成了数据的插入操作。

“二叉树”的概念正如其名,就相当于一颗树。不过这棵树与自然界中的树稍有不同,二叉树从树干开始分杈。树枝上又有分杈,单每次都只会分为两杈,在每个分杈点上有一片叶子(相当于数据)。

要点5:了解栈和队列的实现方法

栈和队列的相似点在于,他们都可以把不能立刻处理的数据暂时存储起来;不同点在于,栈对所存储的数据的存取方式是LIFO的,而队列对所存储数据的存储方式为FIFO。同样是数组,处理手段不同,得到的数据结构也会不同,数组有时可以转化为栈,有时可以转化为队列。

在实现栈这种数据结构时,首先要定义一个数组和一个变量。数组中所包含的元素个数就是栈的大小(栈中最多能存放多少数据)。变量中则存储着一个索引,指向存储在栈中最顶端的数据,该变量被称为“栈顶指针”。栈的大小可以根据需求任意指定。

char Stack[100]; /**作为栈本质的数组 */
char StackPointer = 0 /**栈顶指针 */

/**入栈函数 */
void Push(char Data){
  /**把数据存到栈顶指针所指的位置上 */
  Stack[StackPointer] = Data;
  /**更新栈顶指针的值 */
  StackPointer ++;
}

void Pop(){
  /**更新栈顶指针的值 */
  StackPointer --;
  /**把数据从栈顶指针所指的位置上取出 */
  return Stack[StackPointer]
}

为了实现队列这种数据结构,以下元素是必不可少的:1.一个任意大小的数组;2.一个用于存放排在队头的数据对应的索引的变量;3.一个用于存放排在队尾的数据对应的索引的变量;4.一队函数,分别用于把数据存在队列中和从队列中取出来。如果数据一直存放到了数组的末尾,那么下一个存储位置就会折会到数组的开头。这样就相当于数组的末尾和开头连接上了,于是虽然数组的物理结构是“直线”,但其逻辑结构已经变成了“圆环”了。

char Queue[100];
char SetIndex = 0;
char GetIndex = 0;

/**存储数据的函数 */
void Set(char Data){
  Queue[SetIndex] = Data;
  SetIndex++;
  if(SetIndex >= 100){
    SetIndex = 0;
  }
}

/**读取数据的函数 */
void Get(){
  char Data;
  Data = Queue[GetIndex];
  DetIndex++
  if(SetIndex >= 100){
    SetIndex = 0;
  }
  return Data
}

要点6:了解结构体的组成

要想理解用c语言程序实现链表和二叉树的方法,就必须先了解何为“结构体”。所谓结构体,就是把若干个数据项汇集到一处并赋予其名字后所形成的一个整体。

下列代码,定义了一个叫做TestResult的结构体。C语言中结构体的定义方法是:先在struct这个关键词后面接上结构体的名字,然后在名字后面接上用“{”和“}”括起来的程序块,并在程序块中列出若干个数据项。

struct TestResult{
  char Chinese; /**语文成绩 */
  char Math;    /**数学成绩 */
  char English; /**英语成绩 */
}

一旦定义完结构体,就可以把结构体当作是一种数据类型,用它来定义变量。如果把结构体Test Result用作数据类型并定义了一个名为xiaoming的变量(代表小明的成绩),那么在内存上就相应地分配出了一块空间,这块空间由用于存储Chinese、Math、English这三个成员数据所需地空间汇集而来。被汇集到结构体中的每个数据项都被称作“结构体成员”。在为结构体的成员赋值或是读取成员值时,可以使用形如xiaoming.Chinese(表示小明的语文成绩)的表达式,即以“.”分割变量和结构体的成员

要点7:了解链表和二叉树的实现方法

链表是一种类似数组的数据结构,这个“数组”中的每个元素和另一个元素都好像是手拉着手一样。在现有的以结构体TestResult为数据类型的数组Student[100]中,为了让各个元素“把手拉起来”,就需要在结构体中再添加一个成员。

struct TestResult{
  char Chinese; /**语文成绩 */
  char Math;    /**数学成绩 */
  char English; /**英语成绩 */
  struct TestResult* Ptr /**指向其他元素的指针 */
}

这里的Ptr存储了数组中另一个元素的地址。在C语言中,把存储着地址的变量称为“指针”。这里的“*”就是指针的标志。Ptr就是以结构体TestResult的指针(struct TestResult *)为数据类型的成员。这种特殊的结构体可以称为“自我引用的结构体”。之所以叫这个名字,是因为在结构体TestResult的成员中,含有以TestResult的指针为数据类型的成员,着就相当于TestResult引用了与自身相同的数据类型。

在结构体TestResult的数组中,每个元素都含有一个学生的语文、数学、英语成绩以及成员Ptr。Ptr中存储着本元素接下来该与哪一个元素相连的信息,即下一个元素的地址。在链表的初始状态中,会按照元素在内存上的分布情况设定成员Ptr的值。

因为Ptr中存储的是与下一个元素的连接信息,所以只要替换了Ptr的值,就可以对数组中的元素顺序,使元素的排列顺序不同于其在内存上的物理排列顺序。

明白了链表的构造,也就明白了二叉树的实现方法。在二叉树的实现中,用的还是自我引用的结构体,只不过要改为带有两个连接信息的成员的自我引用结构体。

struct TestResult{
  char Chinese; /**语文成绩 */
  char Math;    /**数学成绩 */
  char English; /**英语成绩 */
  struct TestResult* Ptr1 /**指向其他元素的指针1 */
  struct TestResult* Ptr2 /**指向其他元素的指针2 */
}

二叉树多用于实现那些用于搜索的算法,比如“二分查找法”。比起只使用链表,使用二叉树能更快地找到数据。因为在搜索数据时并不是像在简单数组中那样沿一条线搜索,而是寻着二叉树不断生长出来地两根树杈中的某一枝搜索,这样就能更快地找到目标数据了