序言

掌握常用数据结构和算法是每一位程序员最基本的能力之一。在软件开发领域,**“数据结构+算法=程序”**是大多数开发者的共识,这足以说明数据结构和算法的重要性。数据结构和算法不受编程语言限制,绝大多数编程语言都可以实现大部分的数据结构和算法。掌握常用数据结构和算法可以使我们能够高效地解决一些特定场景的复杂问题。

笔者打算通过《常用数据结构与算法》这个系列梳理一下实际开发中的常用数据结构与算法,并用C#实现对应数据结构的相关算法。这个系列包括数组、链表、栈和队列、树和二叉树、图、堆和哈希表以及排序和查找等数据结构和算法。此外,这个博客系列的难度不会很高,所以各位读者一定要把每篇博客上的示例代码跑一遍并在理解的基础上进行吸收。

数据结构

数据结构(Data Structure)是计算机中存储、组织数据的方式。数据结构是相互之间存在一种或多种特定关系的数据元素的集合,即数据结构是一个带“结构”的数据元素的集合。这个结构就是数据元素之间的关系,分为逻辑结构和存储结构(也称物理结构)。数据的逻辑结构和存储结构是密可不分的两个方面,一个算法的设计取决于逻辑结构,而算法的实现依赖于所采用的的存储结构。

逻辑结构反映了数据元素之间的逻辑关系,与其在计算机中的存储位置无关,即与存储结构无关。逻辑结构分为线性结构和非线性结构。其中,线性表是典型的线性结构,而集合、树和图是典型的非线性结构。逻辑结构具体分为以下4种逻辑结构:

1、集合:结构中的数据元素除“同属于一个集合”的相互关系外,别无其他关系。

2、线性结构:结构中的数据元素存在一对一的相互关系,如数组、链表、栈和队列等。

3、树形结构:结构中的数据元素存在一对多的相互关系,如二叉树等。

4、图形结构:结构中的数据元素存在多对多的相互关系,如有向图和无向图。

存储结构是指数据结构在计算机中的表示(又称映像),也称物理结构。数据的存储结构是逻辑结构用编程语言的实现,它依赖于编程语言。数据的存储结构主要有:顺序存储、链式存储、索引存储和散列存储。下面分别介绍4种存储结构的特点:

1、顺序存储:逻辑相邻的元素存储位置也相邻,如数组。优点是可以随机存取,如数组通过下标索引直接访问对应元素。缺点是只能使用相邻的一整块存储单元,容易出现空缺的存储单元,造成内存空间浪费。

2、链式存储:不要求逻辑相邻的元素存储位置也相邻,通过存有元素地址的指针来链接数据元素,如链表。优点是可以充分利用每一块存储单元,缺点是不能像数组那样随机存取元素,每次存取时只能从头指针开始依次按顺序开始存取元素。

3、索引存储:在存储元素信息时,建立一个附加的索引表。索引表中的每一项称为索引项,一般形式为(关键字,地址)。优点是检索速度来,缺点是索引表会占用额外的内存空间,并且增加和删除元素时都要修改索引表。

4、散列存储:根据元素的关键字通过Hash函数直接计算出元素的存储地址,又称Hash存储,如哈希表。优点是检索、增加和删除元素都很快,但可能会出现哈希冲突,而解决冲突会增加时间和空间的开销。

理解清楚数据的逻辑结构和存储结构,有助于后期数据结构和算法的学习。每一种数据结构都不是万能的,都有自己的适用场景。一个设计良好的数据结构,有利于提高算法的执行效率。

算法

算法(Algorithm)是对特定问题求解步骤的一种描述,是一系列解决问题的指令。算法具有5个重要特性:有穷性(算法的求解步骤和执行时间都是有穷的)、确定性(输入相同输出不变)、可行性、输入(可以没有)和输出(必须有)。算法的效率是通过算法的时间复杂度和空间复杂度来衡量的。

时间复杂度

时间复杂度用T(n)表示,它是问题规模n的函数,定性描述了算法的运行时间。T(n)是算法中所有语句的执行次数之和,分析时间复杂度主要是分析T(n)的数量级。由于算法中关键语句执行次数f(n)与T(n)同数量级,并且算法的运行时间是不能准确得出的,因此我们主要通过f(n)来分析算法的时间复杂度。

因此,T(n) = O(f(n)),称为渐进时间复杂度,简称时间复杂度。其中,大O符号表示f(n)是T(n)是的同数量级函数,且只取函数f(n)的最高阶项且不包括其系数。常见的时间复杂度及大小关系如下图(对数的底可以忽略,即T(n)可以为logn):

下面举一些计算时间复杂度的例子:

1、O(1),没有for循环和while循环等复杂结构。

1
2
3
4
int i = 1;
int j = 1;
i = i + j;
Console.WriteLine(i);

2、O(logn),常见于while循环中。

1
2
3
4
5
6
7
8
int i = 1;
int n = 1024;
while (i < n)
{   
    // i与n的关系实际上是以2为底的指数函数
    i = i * 2;
}
Console.WriteLine(i);

3、O(n),常见于for循环中。

1
2
3
4
5
6
7
8
int j = 1;
int n = 1024;
for (int i = 1; i < n; i++)
{
    // 该语句执行次数与n成正比
    j += i;
}
Console.WriteLine(j);

其他程序的时间复杂度计算与上面的例子相似,主要分析关键语句的执行次数与问题规模n的函数关系,还要注意输入数据的初始状态。

空间复杂度

空间复杂度用S(n)表示,它是问题规模n的函数,定性描述了算法运行所需的存储空间。与时间复杂度T(n)类似,S(n)表示的也是渐进空间复杂度,简称空间复杂度,记为S(n) = O(g(n))。常见的空间复杂度有O(1)和O(n)等。其中,O(1)并不是表示不需要存储空间,而是算法所需存储空间为一个常量。即无论n的规模有多大,算法所需的存储空间是不变的。

下面举一些计算空间复杂度的例子:

1、O(1),无论变量的值有多大,其所需的存储空间是不变的。

1
2
3
4
int i = 1;
int j = 1;
i = i + j;
Console.WriteLine(i);

2、O(n),问题规模n越大,new的内存空间就越大,二者成正比。

1
2
3
4
5
6
int n = 1024;
int[] nums = new int[n];
for (int i = 0; i < n; i++)
{
    nums[i] = i;
}

参考:算法的时间与空间复杂度(一看就懂)