一、什么是算法?
算法,总体概括就是解决问题的方法。它最早产生自《周髀算经》、《九章算术》,那时的算法还不叫算法,称之为“术”,术,是为“方法”,人们在生活中遇到的数学问题,就会思考如何取解决,所以,算法的产生是依靠数学。随着近年来计算机科学的发展,人们发现算法之于计算机的重要性,因为我们需要将解决问题的方法转化为计算机语言,来让计算机实现解决问题的策略。一段好的代码,算法是其核心与灵魂。
那么到底什么是算法呢?这里给出大家公认包括我自己也很喜欢的一个描述:
算法就是任何良定义的计算过程,该过程取某个值或值的集合作为输入并产生某个值或值的集合作为输出。
这样算法就是把输入转换成输出的计算步骤的一个序列。我们也可以把算法看成是用于求解良说明的计算问题的工具。一般来说,问题陈述说明了期望的输入/输出关系。算法则描述一个特定的计算过程来实现该输入/输出关系。
二、算法的特征
1.有穷性:算法必须能在执行有限个步骤之后终止。
2.确切性:算法的每一步骤必须有确切的定义。
3.输入项:一个算法有0个或多个输入,以刻画运算对象的初始情况。所谓0个输入是指算法本身定出了初始条件。
4.输出项:一个算法有一个或多个输出,以反映对输入数据加工后的结果。没有输出的算法是毫无意义的。
5.可行性:算法中执行的任何计算步骤都是可以被分解为基本的可执行的操作步骤,即每个计算步骤都可以在有限时间内完成(也称之为有效性)。
除了上述关键特征之外,还有对于算法问题的特征的描述:
1.存在许多候选解,但大多数候选解都没有解决手头的问题。寻找一个真正的解或一个最好的解可能是一个很大的挑战。
2.存在实际应用。
三、常用的数据结构
先来简单概述一下数据结构。
数据结构实际上是我们自己定义的一种类型,可以用C++中的类或结构体实现。那么我们为什么要定义这种类型呢?因为我们可以利用自定义的类型来有效地管理和组织数据,这种类型里就会包括一些我们需要的信息或属性。既然数据结构可以有效组织数据,算法又需要输入数据,那么数据结构对于算法来说便是一个有利的武器,许多算法也是在特定数据结构的基础上产生和实现的。
下面给出一些常用的数据结构(数据类型这里统一用了int,基本操作这里不再展示),串、数组和广义表不在其中赘述。
3.1 线性结构
3.1.1 顺序表
顺序表实际上就是一个一维数组,加上它的表长。特点是可以随机存取。
typedef struct SqList
{
int* base;//或者可以写成 int array[SIZE];
int length;
}SqList;
3.1.2 链表
普通的单链表,每个结点保存的除了自身数值外,还有下一个结点的指针,把各处的结点通过这样的指针串联起来,像一条链子一样,故称链表。如果加一个可以指向前一个结点的指针,就叫双向链表;如果尾巴重新指回表头,形成一个圈,就叫循环链表;双向链表和循环链表结合还可以有双向循环链表……单链表如下:
typedef struct Lnode
{
int data;
Lnode* next;//指针指向的类型还是自己
}Lnode, *LinkList;
3.1.3 栈
栈与顺序表唯一的区别就是,栈的插入和删除操作只能在表尾进行,正是这种限制,让栈有了“先进后出”的特点。当然你也可以用链栈啦,但是链栈使用频率没有顺序栈高。
typedef struct SqStack//顺序栈
{
sElemType* base;//栈底指针
sElemType* top;//栈顶指针
int stacksize;//栈可用最大容量
}SqStack;
typedef struct StackNode//链栈
{
sElemType data;
struct StackNode* next;
}StackNode, *LinkStack;
同时提一下,栈与递归的联系十分密切,可以说递归就是由栈来实现的。
3.1.4 队列
队列与栈本质相同,同为操作受限的线性表,队列只能在表头删除元素,只能在表尾插入元素,因为这种限制,让队列有了“先进先出”的特点。同样可以用链队列,同样使用频率不高。
typedef struct SqQueue//顺序队列
{
qElemType* base;
int front;
int rear;
}SqQueue;
typedef struct Qnode//链队列中每个结点的类型定义
{
qElemType data;
struct Qnode* next;
}QNode, *QueuePtr;
typedef struct//链队列
{
QueuePtr front;
QueuePtr rear;
}LinkQueue;
3.2 非线性结构
3.2.1 树
树是一种非常典型的非线性结构,也是典型的递归定义同时,它的用处非常大。常用的树形结构是二叉树,这里给出二叉树的链式存储结构定义:
typedef struct BiNode
{
int data;
struct BiNode *lchild, *rchild;//三叉链表会多一个 *parent 指向双亲结点
}BiNode, *BiTree;
对于完全二叉树也可以用数组来存储,这个在堆排序中会有详细介绍。
树的其他表示方法:
typedef struct PTNode//双亲表示法,用数组存
{
int data;
int parent;//双亲位置
}PTNode;
typedef struct CTNode//孩子结点类型定义
{
int child;
struct CTNode *next;
}*ChildPtr;
typedef struct
{
int data;
ChildPtr firstchild;//孩子链表头指针
}CTBox;
typedef struct//孩子链表表示法
{
CTBox nodes[MAXSIZE];
int n,r;//结点数和根结点的位置
}CTree;
typedef struct CSNode//孩子兄弟表示法
{
int data;
struct CSNode *firstchile, *nextsibling;//指向其第一个孩子结点和其下一个兄弟结点
}CSNode, *CSTree;
3.2.2 图
图不同于树,树是一对多,图是多对多。这有几种图的类型定义:
1.邻接矩阵
利用二维数组来表示,有边为1,无边为0。
2.邻接表
利用一个数组,并且数组中的每个元素都是一个表头,连接的是各个与该元素相邻的元素。
3.十字链表、邻接多重表不常用,此处不再赘述。
四、算法分析
这里暂时不过多叙述,分析算法需要有具体的例子,后续具体算法会具体分析。现在这里只解决一个问题,对于大多数刚接触算法的人,会在数据结构与算法中先接触算法这个概念,以及分析算法的概念,而大多数人搞不明白的就是O(n)这个符号,只记得老师说舍弃系数和低阶项以及常数项,事实确实如此,具体证明不给出,说一下原理。
O(g(n)) = { f(n):存在正常量c1、c2和n0,使得对所有n >= n0,有0 <= c1g(n) <= f(n) <= c2g(n) }
并且这里的O记号表示f(n)只有一个渐近上界 ,也就是上述c1g(n)可以省去。这样做其实就是为了通过函数的增长来确定函数的界限,程序执行时,语句的执行次数总和即为时间效率,而语句执行次数可能与输入的规模有关,当与规模有关时,为了更普遍的评测该算法,需要输入数据足够大、各种数据全覆盖,而我们在简单分析的时候,输入规模足够大时,程序的执行次数函数会被夹在两个函数之间,而这两个函数是同一个函数g(n)前乘两个不同常数形成的,我们称g(n)是f(n)的渐近紧确界。
还有很多符号表示不同的含义,比如是表示渐近下界的符号等等,这里不再介绍。
总结一下,O(n)符号可以在输入规模足够大时表示程序的执行次数,从而让我们更方便的观察算法的时间效率。