数据结构与算法是计算机科学中的核心概念,也与现实生活如算法岗息息相关。鉴于全网数据结构文章良莠不齐且集成度不高,故开设本专栏,为初学者提供指引。
在学习数据结构前,必要的概念是需要我们掌握的。在这一篇中,我们着重要讲解的就是数据结构与算法中的各种概念,为后面的学习打下基础。
数据结构
数据结构(data structure)是一种具有一定逻辑关系,在计算机中应用某种存储结构,并且封装了相应操作的数据元素集合。它包含三方面的内容,逻辑关系、存储关系及操作。
不同种类的数据结构适合于不同种类的应用,而部分甚至专门用于特定的作业任务。例如,计算机网络依赖于路由表运作,B 树高度适用于数据库的封装。
数据结构为何面世
随着应用程序变得越来越复杂和数据越来越丰富,几百万、几十亿甚至几百亿的数据就会出现,而对这么大对数据进行搜索、插入或者排序等的操作就越来越慢,数据结构就是用来解决这些问题的。
算法
概念
算法是在一类特定的数据模型上定义所有运算并以解决一类特定问题为目标的一个有限的运算序列,它与数据结构是息息相关的。
算法实现的三要素
1.数据:算法需要处理的数据是算法的输入,也是算法的输出。数据包括各种数据类型(如整数、浮点数、字符串、数组、链表、树等),以及数据结构(如栈、队列、堆、图等)。
2.运算:算法的核心是运算,包括各种基本运算(如加、减、乘、除、取模等)、比较运算(如大于、小于、等于等)、逻辑运算(如与、或、非等)等。此外,算法还可以包括一些高级运算(如快速幂、快速排序、动态规划等),以及各种库函数的调用。
3.控制:算法的执行流程需要通过控制来实现。控制包括顺序结构、选择结构、循环结构和函数调用等。顺序结构表示按照代码的书写顺序依次执行各个语句;选择结构包括if语句和switch语句,用于根据条件选择不同的分支;循环结构包括for循环、while循环和do-while循环,用于重复执行某个代码块;函数调用用于将代码分解成多个可重用的模块,提高代码的可读性和可维护性。
算法的描述载体:自然语言、数据流图、程序语言或者伪代码
算法的五大特征:
输入:具有零个或多个输入,这些输入取自特定的数据对象集合
输出:至少具有一个或多个输出,这些输出同输入之间存在某种特定的关系
确定性(双重含义):组成算法的每条指令是清晰的、无歧义的;无二义性,对应相同的输入仅有唯一的一条算法执行路径
有限性:序列项数有限且每一项运算时间有限
可行性:∀合法输入 ∃正确输出
基本数据类型
数据类型是指高级程序设计语言中,用以刻划程序中操作对象的特性。类型显式地或隐含地规定了在程序执行期间变量或表达式所有可能的取值范围,以及在这些值上允许进行的操作。
抽象数据类型
抽象数据类型是基本数据类型概念的引伸和发展,指操作对象的一个数据模型以及定义在该模型上的一组操作。也就是说,对于抽象数据类型的描述,除了必须描述它的数据结构外,还必须描述定义在它上面的运算(过程或函数)。
抽象数据类型的内容需要:约定抽象数据类型的名字 、约定在该类型上定义的一组运算的各个运算的名字 、明确各个运算分别有多少个参数、这些参数的含义和顺序以及运算的功能
抽象数据类型的目标:把数据类型的表示和数据类型上运算的实现,与其在程序中的应用分开,相互独立 ;顶层和底层都与抽象数据类型的定义打交道;算法底层的设计就是数据结构的设计和函数的设计。
抽象数据类型的优势
顶层设计和底层实现分离、算法设计和数据结构设计分离 、数据模型和运算的内在统一于抽象数据类型之中、局部化、模块化、编出来的程序结构清晰,层次分明,便于程序正确性的证明和复杂性的分析
常见的数据结构
栈(Stack): 栈是一种特殊的线性表,它只能在一个表的一个固定端进行数据结点的插入和删除操作。
队列(Queue): 队列和栈类似,也是一种特殊的线性表。和栈不同的是,队列只允许在表的一端进行插入操作,而在另一端进行删除操作。
数组(Array): 数组是一种聚合数据类型,它是将具有相同类型的若干变量有序地组织在一起的集合。
链表(Linked List): 链表是一种数据元素按照链式存储结构进行存储的数据结构,这种存储结构具有在物理上存在非连续的特点。
树(Tree): 树是典型的非线性结构,它是包括 2 个结点的有穷集合 K。
图(Graph): 图是另一种非线性数据结构。在图结构中,数据结点一般称为顶点,而边是顶点的有序偶对。
堆(Heap): 堆是一种特殊的树形数据结构,一般讨论的堆都是二叉堆。
散列表(Hash table): 散列表源自于散列函数(Hash function),其思想是如果在结构中存在关键字和T相等的记录,那么必定在F(T)的存储位置可以找到该记录,这样就可以不用进行比较操作而直接取得所查记录。
常用算法
数据结构研究的内容就是如何按一定的逻辑结构,把数据组织起来,并选择适当的存储表示方法把逻辑结构组织好的数据存储到计算机的存储器里。算法研究的目的是为了更有效的处理数据,提高数据运算效率。数据的运算是定义在数据的逻辑结构上,但运算的具体实现要在存储结构上进行。一般有以下几种常用运算:
- 检索: 检索就是在数据结构里查找满足一定条件的节点。一般是给定一个某字段的值,找具有该字段值的节点。
- 插入: 往数据结构中增加新的节点。
- 删除: 把指定的结点从数据结构中去掉。
- 更新: 改变指定节点的一个或多个字段的值。
- 排序: 把节点按某种指定的顺序重新排列。例如递增或递减。
以上我们引入了算法、数据结构、数据类型等概念,而要想衡量一个算法与数据结构是否为优质的,就需要一个衡量标准,这个衡量标准也是在我们实现一个好的算法时要遵循的原则。
所以接下来,我们要介绍算法复杂度。
算法复杂度
算法复杂度是衡量算法效率的指标,它描述了算法运行时间或空间需求随着输入规模增加而增加的趋势。通常分为时间复杂度和空间复杂度两种。
时间复杂度描述了算法解决问题所需的计算时间与输入规模之间的关系。常用的时间复杂度包括常数阶O(1)、对数阶O(logn)、线性阶O(n)、线性对数阶O(nlogn)、平方阶O(n^2)等,其中O表示“大O记号”。
空间复杂度描述了算法解决问题所需的内存空间与输入规模之间的关系。类似地,常用的空间复杂度也可以用大O记号表示。
由于空间复杂度较为简单,本文主要介绍时间复杂度。
渐进性态
算法复杂度的渐进性态是指随着问题规模的增大,算法的执行时间或者空间占用的增长趋势。通常情况下,我们关注的是算法在最坏情况下的表现,因为这可以给我们一个最保守的估计。
渐进性态数学表征
上界(Upper bound):使用大 O 符号(O)表示算法的上界,描述了算法执行时间或空间占用随输入规模增长的最大限制。它给出了算法在最坏情况下的表现。
下界(Lower bound):使用大 Ω 符号(Ω)表示算法的下界,描述了算法执行时间或空间占用随输入规模增长的最小限制。它给出了算法在最好情况下的表现。下界表示算法至少需要这么多的资源来解决问题。
平均情况(Average case):使用大 Θ 符号(Θ)表示算法的平均情况,描述了算法在所有可能输入情况下的平均表现。它给出了算法的期望性能。
通常情况下,我们更关注算法的上界和平均情况,因为它们提供了对算法性能的更全面评估。下界通常用于证明某个算法的最佳性能限制。
复杂度排序:Ο(1)<Ο(logn)<Ο(n)<Ο(nlogn)<Ο(n2)<Ο(n3)<Ο(2^n)
算法复杂度的运算
我们通常以执行次数来匹配时间复杂度,看如下代码:
for (i = 0; i < n - 1; i++) { for (j = 0; j < n - i - 1; j++) { if (sorted_arr[j] > sorted_arr[j + 1]) { int temp = sorted_arr[j]; sorted_arr[j] = sorted_arr[j + 1]; sorted_arr[j + 1] = temp; } } }
算法中使用了两个嵌套的 for 循环,执行了 n^2 次循环操作,因此,我们可以说这个算法的时间复杂度是 O(n^2)
一般来说,在运算时,遵循以下规则:
(1)可以忽略加法常数 O(3n + 131) 相当于 O(3n) (2)与最高次项相乘的常数可忽略 O(88n^3) 相当于 O(n^3) (3) 最高次项的指数大的,函数随着 n 的增长,结果也会变得增长得更快 O(n^8) > O(n^4) (4)判断一个算法的(时间)效率时,函数中常数和其他次要项常常可以忽略,而更应该关注主项的阶数 将O(3n^3+3n+1)的常数与次要项忽略,得到O(n^3)
下面我们已两种算法来介绍对时间复杂度的化简。
顺序搜索算法
顺序搜索算法(Sequential Search Algorithm),也称为线性搜索算法,是一种简单直观的搜索方法。它逐个地检查待搜索的元素,直到找到目标元素或遍历完整个数据集。
示例如下:
#include <stdio.h> int sequential_search(int arr[], int n, int target) { int i; for (i = 0; i < n; i++) { if (arr[i] == target) { return i; // 返回目标元素的索引 } } return -1; // 目标元素未找到 } int main() { int arr[] = {2, 5, 8, 10, 13, 6}; int n = sizeof(arr) / sizeof(int); int target = 10; int result = sequential_search(arr, n, target); if (result != -1) { printf("目标元素 %d 在数组中的索引为 %d\n", target, result); } else { printf("目标元素 %d 未找到\n", target); } }
最好的情况即为:第一个元素就是要找的元素,常数时间O(1),最坏的情况即为:元素不在数组中,执行n次,线性时间O(n)。得到平均时间复杂度为最好情况与最坏情况对应的复杂度之和的一半。即o((1+n)/2),省略常数得到o(n)
二分搜索算法
二分搜索算法(Binary Search Algorithm),也称为折半搜索算法,是一种高效的搜索方法,它利用了已经排好序的数组的特点。它首先将待搜索区域缩小为一半,然后检查中间元素,如果目标元素等于中间元素,则找到目标;如果目标元素小于中间元素,则在左半部分继续搜索;如果目标元素大于中间元素,则在右半部分继续搜索。通过反复缩小搜索区域,最终可以找到目标元素或确定其不存在于数组中。
示例如下:
#include <stdio.h> int binary_search(int arr[], int n, int target) { int left = 0; int right = n - 1; while (left <= right) { int mid = left + (right - left) / 2; if (arr[mid] == target) { return mid; // 返回目标元素的索引 } else if (arr[mid] < target) { left = mid + 1; // 在右半部分继续搜索 } else { right = mid - 1; // 在左半部分继续搜索 } } return -1; // 目标元素未找到 } int main() { int arr[] = {2, 5, 6, 8, 10, 13}; int n = sizeof(arr) / sizeof(int); int target = 8;int result = binary_search(arr, n, target); if (result != -1) { printf("目标元素 %d 在数组中的索引为 %d\n", target, result); } else { printf("目标元素 %d 未找到\n", target); } return 0; }
该算法的原理是每次将搜索区间减半,因此需要进行logn次比较,所以时间复杂度是O(logn)。
以上就是本篇的内容了,在下一篇中我们会接触这些基础概念的相关练习,帮你更好地巩固知识点。