数据结构与算法 01、 基本认知 & 线性表初探
本文用来了解数据结构与算法的一些基础。
书籍推荐:《大话数据结构 - 程杰 著》
一、算法
1、算法特性
算法:解决特定问题的一步步思路方法步骤。在计算机中其表现在指令的有限序列,每个指令包含一或多个操作。
算法特性:
- 有输入输出
- 有穷性 - 有限步骤内完成,不能无限循环
- 确定性 - 有确定结果,不能存异
- 可行性 - 每一行代码必须示可执行的
算法的设计要求:
- 正确性 -
- 可读性 -
- 健壮性 -
- 时间效率高&存储量低
2、算法的复杂度
大O表示法
- 用常数1取代运行时间中所有常数 3 --> O(1)
- 运行次数函数中,只保留最高阶 n3+2n2+3 --> O(n3)
- 最高阶存在且不等于1时,去除与这个高阶项 相乘的常数 3n3+2n2+n+4 --> O(n3)
1、时间复杂度
时间复杂度术语:
- 常数阶 - 3
- 对数阶 - logan --> 以 a 为底 n 的对数 ax=n, x=logan
- 线性阶 - 2n+1
- 平方阶 - n2
- 立方阶 - n3
- nlog 阶 - n*logn 即 n * log2n. 一般以2为底习惯性省略2的书写.
- 指数阶 - O(2n) 或者 O(n!) --> 一般不考虑!除非是 n 非常的小,否则会造成可怕的噩梦般的时间消耗。这是一种不切实际的算法时间复杂度,一般不会考虑。
1. 示例
时间复杂度大小关系:O(1) < O(logn) < O(n) < O(nlogn) < O(n2) < O(n3) < O(2n) < O(n!) < O(nn)
2. 代码示例
常数阶:
// 常数阶时间复杂度计算 O(1) <-- 常数用 1 代替 // 1+1+1 = 3 --> O(1) void testSum1(int n){ int sum = 0; // 执行1次 sum = (1+n)*n/2; // 执行1次 printf("testSum1:%d\n",sum);// 执行1次 } // 1+1+1+1+1+1+1 = 7 --> O(1) void testSum2(int n){ int sum = 0; // 执行1次 sum = (1+n)*n/2; // 执行1次 sum = (1+n)*n/2; // 执行1次 sum = (1+n)*n/2; // 执行1次 sum = (1+n)*n/2; // 执行1次 sum = (1+n)*n/2; // 执行1次 printf("testSum2:%d\n",sum);// 执行1次 } // x=x+1; --> 执行1次 void add(int x){ x = x+1; }
对数阶:
// 对数阶 -- 不论是 以几为底n的对数,都会归到 logn 表示 // 2的x次方等于n x = log2n --> O(logn) void testA(int n){ int count = 1; //执行1次 // n = 10 while (count < n) { count = count * 2; } }
// 5的x次方等于n x = log5n --> O(logn) void testF(int n){ int count = 1; //执行1次 // n = 100 while (count < n) { count = count * 5; } }
线性阶:
// 线性阶时间复杂度 // x=x+1; 执行n次 --> O(n) void add2(int x,int n){ for (int i = 0; i < n; i++) { x = x+1; } } // 1+(n+1)+n+1 = 3+2n --> O(n) void testSum3(int n){ int i,sum = 0; // 执行1次 for (i = 1; i <= n; i++) { // 执行n+1次 sum += i; // 执行n次 } printf("testSum3:%d\n",sum); // 执行1次 }
平方阶:
// 平方阶 // x=x+1; 执行 n*n 次 --> O(n^2) void add3(int x,int n){ for (int i = 0; i< n; i++) { for (int j = 0; j < n ; j++) { x=x+1; } } } // n+(n-1)+(n-2)+...+1 = n(n-1)/2 = n^2 / 2 + n/2 --> O(n^2) // 等差数列公式:sn = n(a1+an)/2 <-- (1+100)*100/2 = 5050 void testSum4(int n){ int sum = 0; for(int i = 0; i < n;i++) for (int j = i; j < n; j++) { sum += j; } printf("textSum4:%d",sum); } // 1+(n+1)+n(n+1)+n^2+n^2 = 2+3n^2+2n --> O(n^2) void testSum5(int n){ int i,j,x=0,sum = 0; // 执行1次 for (i = 1; i <= n; i++) { // 执行n+1次 for (j = 1; j <= n; j++) { // 执行n(n+1) x++; // 执行n*n次 sum = sum + x; // 执行n*n次 } } printf("testSum5:%d\n",sum); }
立方阶:
// 立方阶 void testB(int n){ int sum = 1; // 执行1次 for (int i = 0; i < n; i++) { // 执行n次 for (int j = 0 ; j < n; j++) { // 执行n*n次 for (int k = 0; k < n; k++) {// 执行n*n*n次 sum = sum * 2; // 执行n*n*n次 } } } }
3. 时间复杂度的大小
2、空间复杂度
空间复杂度:计算算法过程中,中间变量所需的存储空间。
公式 S(n) = n(f(n)) --> n:问题的规模; f(n):关于 n 所占存储空间的函数
程序空间计算考虑因素:
- 寄存本身的指令
- 常数
- 变量
- 输入
- 对数据进行操作的辅助空间
在考量算法的空间复杂度时,主要考虑的是算法执行时所需要的辅助空间。
代码示例:
// 数组逆序,将一维数组 a 中的 n 个数逆序存放在原数组中. int main(int argc, const char * argv[]) { // insert code here... printf("Hello, World!\n"); int n = 5; int a[10] = {1,2,3,4,5,6,7,8,9,10}; // 算法实现1 --> O(1) int temp;// 1中间变量 for(int i = 0; i < n/2 ; i++) { temp = a[i]; a[i] = a[n-i-1]; a[n-i-1] = temp; } for(int i = 0;i < 10;i++) { printf("%d\n",a[i]); } // 算法实现2 --> O(n) int b[10] = {0};// n for(int i = 0; i < n;i++) { b[i] = a[n-i-1];// n个中间值 } for(int i = 0; i < n; i++) { a[i] = b[i]; } for(int i = 0;i < 10;i++) { printf("%d\n",a[i]); } return 0; }
二、数据结构 概念
文章 数据结构与算法 0 中对数据结构的一些基本概念进行了简单了解,这里进行更详细的介绍。
1、基本数据单位
2、逻辑结构与物理结构
1)逻辑结构
逻辑结构,描述的是数据元素之间的相互关系。它只是代表数据间所存在的一个逻辑关系,例如:人与人、树干与树枝等。
1. 集合结构
集合结构:同属于一类但并无什么特定关系。例:动物便是一个集合,包含人、猫猫、狗狗...;植物也是一个集合,花草树木...。
2. 线性结构
线性结构:数据与数据间符合一对一的关系。
常见线性结构:线性表、链表、数组、字符串(多个字符组合而成);
队列、栈 因其特殊的读取方式,属于特殊的线性结构。
3. 树结构
树结构特点:数据关系之间是一对多的关系。
4. 图形结构
图形结构特点:数据与数据间多对多的关系。
2)物理结构(也称存储结构)
物理结构:指数据的逻辑结构在计算机中的存储形式。
数据是数据元素的集合,那么物理结构实际上就是:如何把数据元素存储到计算机的存储器中。存储器是针对内存而言的。(硬盘、软盘、光盘等外部存储器的数据组织通常用文件结构来描述。)
对于所有数据,最终它们都是需要存储到计算机内存中去的,数据元素在内存中的存储方式(存储结构形式)有 2 种:顺序存储 链式存储。
1. 顺序存储结构
开辟一段连续的内存空间,按顺序依次存储进去,其数据间的逻辑关系和物理关系是一致的。简单来说就是:排队占位。按序排好队,每人占一小段空间。例:数组 arr[10].
优点:查询快;
缺点:增删麻烦,每次都需要对数据进行重新的排序。
2. 链式存储结构
链式存储结构:把数据元素存放在任意的存储单元里,这组存储单元可以是连续的也可不连续,即:不需要提前开辟连续的内存空间。
这种数据元素的存储关系并不能反映其逻辑关系,因此需要用一个指针存放数据元素的地址,这样就可以通过地址找到相关元素的位置。
优点:增删便捷,增加找到位置放进去即可,删除后删除的元素需要释放为nil防止野指针;
缺点:查询很慢。
3、抽象数据类型
1. 数据类型
数据类型:是指一组性质相同的值的集合 以及 定义子在此集合上的一些操作的总称。
数据类型是按照值的不同进行划分的。在 C 语言中,按照值的不同,数据类型可分为 2 类:
- 原子类型:不可再分解的基本类型,包括:整型、实型、字符型等;
- 结构类型:由若干个类型组合而成,可以再分解。例如:整形数组,它是由若干整型数据组成的。
例如 int a ,b; 这意味着 在给变量 a、b 赋值时不能超出 int 的取值范围,且变量之间的运算只能是 int 类型所允许的运算。
因不同计算机不同的硬件系统,编程语言最终都会被编译成底层语言。然而对于高级语言来讲,目的知识实现 a+b 的运算,不关心计算机内部或 CPU 都具体做了什么,这里其实就引出了 抽象 的概念。
抽象:是指抽取出事物具有的普遍性的本质。它抽出问题的特征而忽略非本质的细节,相当于是对一个具体事物的概括。即:隐藏了繁杂的细节,保留实现目标所必须的信息。例如:一本书籍的章节目录。
2. 抽象数据类型
抽象数据类型(Abstract Data Type, ADT):指 一个数学模型 及其 定义在该模型上的一组操作。抽象数据类型的定义仅仅取决于它的一组逻辑特性,与计算及内部如何表示和实现无关。
比如上述的 整型 int 的例子,各个计算机,不论电脑、pad、手机等等,都拥有“整型”类型,也需要证书间的运算,so 整型 其实就是一个抽象数据类型。
除了已定义并实现的,抽象数据类型还包括编程者自己定义的数据类型,如坐标,定义一个 point 的抽象数据类型,它有 x、y、z 三个整型变量,我们就可以通过 point 数据变量来知道这一点的坐标。
抽象数据类型体现了程序设计中 问题分解、抽象和信息隐藏的特性。
描述抽象数据类型的标准格式:
/**描述抽象数据类型的标准格式 ADT 抽象数据类型名 Data 数据元素之间的逻辑关系定义 Operation 操作1 初始条件 操作结果描述 操作2 ... ... 操作 n ... ... endADT */
三、线性表的存储方式
线性表(List):零个或多个数据元素的有序序列。有序 有限.
如下图,将线性表用数学语言进行定义,记为:(a1, ..., ai-1, ai, ai+1, ..., an),则表中 ai-1 领先于 ai, ai 领先于 ai+1,称 ai-1 是 ai 的直接前驱元素,ai+1 是 ai 的直接后继元素。当 i=1,2,...,n-1 时,ai 有且仅有一个直接后继,当 i=2,3,...,n 时,ai 有且仅有一个直接前驱。
由上可知,线性表元素的个数 n(n≥0) 定义为线性表的长度,当 n=0 时,称为空表。
对于非空的线性表和线性结构,特点如下:
- 存在唯一的一个被称作“第一个”的数据元素;
- 存在唯一的一个被称作“最后一个”的数据元素;
- 除了第一个之外,结构中的每个元素都有一个前驱;
- 除了最后一个外,结构中的每个元素都有一个后继。
线性表的抽象数据类型:
/**线性表的抽象数据类型定义 ADT 线性表 (List) Data 线性表的数据对象集合为{a1, a2, ..., an},每个元素的类型均为 DataType。其中,除第一个元素 a1 外,每个元素都有且仅有一个直接前驱元素,除最后一个元素 an 外,每个元素都有且仅有一个直接后继元素。数据元素之间的关系是一对一。 Operation InitList (*L) :初始化操作,建立一个空的线性表 L ListEmpty (L) :若线性表为空,返回 true,否则返回 false ClearList (L) :将线性表清空 GetElem(L, i, *e) : 将线性表 L 的第 i 个位置元素值返回给 e LocateElem(L, e) : 在线性表 L 中查找与给定值 e 相等的元素,成功返回该元素在表中序号;否则返回 0 表示查找失败 ListInsert(*L, i, e) : 在线性表 L 中 第 i 个位置插入新元素 e ListDelete(*L, i, *e) : 删除线性表 L 中 第 i 个位置元素,并用 e 返回其值 ListLength(L) :返回 线性表 L 的元素个数 endADT */
1、线性表的顺序存储
线性表的顺序存储结构,指 用一段地址连续的存储单元依次存储线性表的数据元素。
1. 代码示例
顺序存储的线性表,创建、初始化和增删:
1 /// 顺序存储的线性表 2 3 #define MAXSize 100 4 5 typedef struct { 6 int *data; 7 int length; 8 }MyList; 9 10 MyList gList; 11 12 // 初始化一个线性表 13 int initList(MyList *L) { 14 15 L->data = malloc(sizeof(MyList)*MAXSize); 16 if (!L->data) {// 若分配空间失败,退出 17 exit(0); 18 } 19 L->length = 0;// 空表长度0 20 return 1; 21 } 22 23 // 线性表的插入 24 /** 25 在 L 中 的 i 位置 添加插入 ele 26 插入位置的后面的元素都要后移一位 27 */ 28 int insertList(MyList *L, int i, int ele) { 29 30 if (i < 0 || i > L->length + 1) {// 位置合法 31 exit(0); 32 } 33 if (L->length == MAXSize) {// 最大空间已满 34 return 0; 35 } 36 37 if (i <= L->length) { 38 for (int j = L->length-1; j >= i; j--) { 39 // 添加的元素 i 位置后的元素都后移一位 40 L->data[j+1] = L->data[j]; 41 } 42 } 43 L->data[i] = ele; 44 ++L->length;// 表长度+1 45 return 1; 46 } 47 48 // 线性表的取值 49 // 取某个值 50 int getElemFrmList(MyList L, int i, int *elem) { 51 52 if (i < 0 || i > L.length + 1) {// 位置合法 53 exit(0); 54 } 55 *elem = L.data[i-1]; 56 return 1; 57 } 58 // 打印全部值 59 int getAllEleFormList(MyList l){ 60 for (int i=0; i<l.length; i++) { 61 printf("-- list的%d元素:%d --\n",i,l.data[i]); 62 } 63 return 1; 64 } 65 66 // 删除 67 int deleteList(MyList *L, int i, int *ele) { 68 69 if (L->length==0 || i < 0 || i > L->length + 1) {//线性表不为空 / 位置合法 70 exit(0); 71 } 72 *ele = L->data[i]; 73 for (int j = i; j<L->length; j++) { 74 // 删除元素的位置开始,后面的元素依次向前一位 75 L->data[j] = L->data[j+1]; 76 } 77 --L->length;// 表长度-1 78 return 1; 79 } 80 81 // 清空 82 int clearList(MyList *l) { 83 l->length = 0; 84 return 1; 85 } 86 87 int main(int argc, const char * argv[]) { 88 @autoreleasepool { 89 // insert code here... 90 NSLog(@"Hello, World!"); 91 92 93 MyList L; 94 int status; 95 96 // 初始化 97 status = initList(&L); 98 printf("初始化的线性表 length=%d\n",L.length);// 0 99 // 插入 100 for (int i=0; i<5; i++) { 101 status = insertList(&L,i,i+10);// 10 11 12 13 14 102 } 103 printf("线性表 length=%d\n",L.length);// 5 104 // 取值 105 int ele;// 变化的数据用它的指针来指 106 status = getElemFrmList(L, 2, &ele); 107 printf("线性表的第2个元素=%d\n",ele);// 11 108 getAllEleFormList(L); 109 // 删除元素 110 status = deleteList(&L, 3, &ele); 111 printf("线性表的删除的第3个元素是=%d length=%d\n",ele,L.length);// 13 112 // 清空list 里的元素 113 status = clearList(&L); 114 printf("线性表 length=%d\n",L.length);// 0 115 } 116 return 200; 117 }
2. 线性表顺序存储结构的优缺点
优点:
- 无需额外为表示表中元素之间的逻辑关系而增加额外的存储空间;
- 可以快速的存取表中任一位置的元素。
缺点:
- 插入和删除操作需要移动大量元素;
- 当线性表长度变化较大时,难以确定存储空间的容量;
- 造成存储空间碎片。
2、线性表的链式存储
我们已知顺序存储的最大缺点是插入删除时需要移动大量数据元素,这显然是要耗时的,如何解决呢?--> 链式存储结构
文章 数据结构与算法 0 中链表部分有通过简单图形介绍无头结点的链表结构。
1. 带有头结点的链表:
2. 代码示例
1)带有头结点的单链表的 创建初始化、插入、删除、取值、遍历:
1 // 方便起见,类型 typedef 一下 2 typedef struct Node { 3 4 int data; 5 struct Node *next; 6 } Node; 7 typedef struct Node * linkedList; 8 9 // 初始化一个空链表 10 int func_initLinkedList(linkedList *L) { 11 12 *L = (linkedList)malloc(sizeof(Node)); 13 if (*L == NULL) { 14 return 0; 15 } 16 (*L)->next = NULL; 17 return 200; 18 } 19 20 // 单链表插入数据 21 int func_insertLinkedList (linkedList *L, int i, int e) { 22 23 linkedList p,s; 24 p = *L; 25 26 int j = 1; 27 28 // 找到要插入位置的前一个结点 29 while (p && j<i) { 30 p = p->next; 31 ++j; 32 } 33 if (!p || j>i) {// p不存在 或 插入位置不合法 34 return 0; 35 } 36 37 // 建一个新节点 38 s = (linkedList)malloc(sizeof(Node)); 39 s->data = e; 40 // 新结点 s 的 next 指向 p的后继 41 s->next = p->next; 42 // p 的后继指向 新结点 s 43 p->next = s; 44 return 200; 45 } 46 47 // 取值 48 int func_getElem(linkedList L, int i, int *e) { 49 50 int j; 51 linkedList p; 52 p = L->next; 53 j = 1; 54 while (p && j<i) { 55 p = p->next; 56 j++; 57 } 58 if (!p || j>i) {// p不存在 或 插入位置不合法 59 return 0; 60 } 61 *e = p->data; 62 return 200; 63 } 64 65 // 遍历 66 int func_allElem(linkedList L) { 67 68 linkedList p; 69 p = L->next; 70 71 while (p) { 72 printf("getall: %d\n",p->data);/** 73 getall: 9 74 getall: 8 75 getall: 7 76 getall: 6 77 getall: 5 78 getall: 4 79 getall: 3 80 getall: 2 81 getall: 1 82 */ 83 p = p->next; 84 } 85 return 200; 86 } 87 // 删除结点 88 int func_deleteNode(linkedList *L, int i, int *e) { 89 90 linkedList p,s; 91 int j = 1; 92 p = (*L)->next; 93 94 // 找到要删除结点的前一个结点 95 while (p->next && j<(i-1)) { 96 p = p->next; 97 ++j; 98 } 99 if (!(p->next) || j>i-1) {// p不存在 或 删除位置不合法 100 return 0; 101 } 102 103 s = p->next;// 要删除的结点 s 104 p->next = s->next; 105 106 *e = s->data; 107 free(s); 108 109 return 200; 110 } 111 112 113 // 链式存储的线性表 114 void func_linkedList (){ 115 116 linkedList L; 117 struct Node *L1; 118 func_initLinkedList(&L); 119 func_initLinkedList(&L1); 120 121 for (int i = 1; i<10; i++) { 122 func_insertLinkedList(&L, 1, i); 123 }// L:9 8 7 6 5 4 3 2 1 124 125 int e; 126 func_getElem(L,3,&e); 127 printf("e: %d\n",e);// e: 7 128 129 func_allElem(L); 130 131 int e1; 132 // 删除第 4 个 133 func_deleteNode(&L, 4, &e1); 134 printf("e1: %d\n",e1);// e1: 6 135 func_allElem(L); 136 137 }
2)单链表头插法和尾插法:
1 // 头插法 - 插入n个结点 2 int func_addHead(linkedList *L,int n) { 3 4 // 建立带头结点的单链表 5 *L = (linkedList)malloc(sizeof(Node)); 6 (*L)->next = NULL; 7 8 linkedList p; 9 for (int i=0; i<n; i++) { 10 11 p = (linkedList)malloc(sizeof(Node)); 12 p->data = i; 13 p->next = (*L)->next; 14 (*L)->next = p; 15 } 16 return 200; 17 } 18 // 尾插法 19 int func_addTail (linkedList *L,int n) { 20 21 // 建立带头结点的单链表 22 *L = (linkedList)malloc(sizeof(Node)); 23 (*L)->next = NULL; 24 linkedList t; 25 t = *L;// t 为尾结点 26 27 linkedList p; 28 for (int i=0; i<n; i++) { 29 p = (linkedList)malloc(sizeof(Node)); 30 p->data = i; 31 32 t->next = p; 33 t = p; 34 } 35 t->next = NULL; 36 37 return 200; 38 } 39 40 // 调用 41 printf("\n"); 42 // 头插法 43 linkedList L3; 44 func_addHead(&L3, 10); 45 func_allElem(L3);// 9 8 7 6 5 4 3 2 1 0 46 47 printf("\n"); 48 49 // 尾插法 50 linkedList L4; 51 func_addTail(&L4, 10); 52 func_allElem(L4);// 0 1 2 3 4 5 6 7 8 9
3)清空链表
1 // 清空链表 2 int func_clearLinkedList(linkedList *L){ 3 4 linkedList p,tmp; 5 p = (*L)->next;// p 指向第一个结点 6 while (p) { 7 tmp = p->next; 8 free(p); 9 p = tmp; 10 } 11 (*L)->next = NULL;// 头结点指针域置为空 12 return 200; 13 }
以上。