第一章 绪论
-
理解基本概念和术语
数据:数据是对客观事物的符号表示,在计算机科学中是指所有能输入到计算机中并被计算机程序所处理的符号的总称,数字,字符,图形,声音等
数据元素:数据元素是数据的基本单位,通常作为一个整体进行考虑和处理,也称之为元素,结点,顶点记录。(补充:一个数据元素可由若干个数据项组成。数据项是数据的不可分割的最小单位。)
数据对象:数据对象是具有相同特性的数据元素的结合,是一个数据的子集
数据结构:数据结构是相互之间存在一种或多种特定关系的数据元素的集合。
(1)数据的逻辑结构:数据的逻辑结构是指数据元素之间存在的固有逻辑关系,常称为数据结构。数据的逻辑结构是从数据元素之间存在的逻辑关系上描述数据与数据的存储无关,是独立于计算机的:
- 集合:数据中的数据元素之间除了“同属于一个集合“的关系以外,没有其他关系。
- 线性结构:结构中的数据元素之间存在“一对一“的关系。
- 树形结构:结构中的数据元素之间存在“一对多“的关系。
- 图状结构(网状结构):结构中的数据元素存在“多对多”的关系。
(2)数据的存储结构(物理结构):数据元素及其关系在计算机内的表示称为数据的存储结构。想要计算机处理数据,就必须把数据的逻辑结构映射为数据的存储结构:
- 顺序存储结构:把逻辑上相邻的数据元素存储在物理位置也相邻的存储单元中,借助元素在存储器中的相对位置来表示数据之间的逻辑关系。
- 链式存储结构:借助指针表达数据元素之间的逻辑关系。不要求逻辑上相邻的数据元素物理位置上也相邻。
- (补充)索引存储方式 (关键字,地址)
- (补充)哈希存储方式 关键字通过函数计算
数据类型:一个值的集合和定义在这个值集上的一组操作的总称。(eg.C语言的整型变量)
抽象数据类型(ADT):是指一个数学模型和定义在此数学模型上的一组操作。(eg.整数)仅取决于逻辑特性,与计算机内部如何实现无关,不论内部结构如何变化,只要数学特性不变,都不影响外部使用
算法:是对特定问题求解步骤的描述,是指令的有限序列。
算法的5个特性:
- 输入:有零个或多个输入
- 输出:有一个或多个输出
- 有穷性:要求序列中的指令是有限的;每条指令的执行包含有限的工作量;整个指令序列的执行在有限的时间内结束。(程序与算法的区别在于,程序不需要有有穷性)
- 确定性:算法中的每一个步骤都必须是确定的,算法只有唯一的一条执行路径。
- 可行性:算法中的每一个步骤都应当能被有效的执行,并得到确定的结果。
算法设计的要求:
- 正确性、
- 健壮性(能处理合法数据,也能对不合法的数据作出反应,不会产生莫名其妙的输出结果)
- 可读性(要求算法易于理解,便于分析)
- 效率与低存储量需求
时间复杂度分析: 1.常量阶:算法的时间复杂度与问题规模n无关系T(n)=O(1)
2.线性阶:算法的时间复杂度与问题规模n成线性关系T(n)=O(n)
3.平方阶和立方阶:一般为循环的嵌套,循环体最后条件为i++
时间复杂度的大小比较:
O(1)<O(log2n)<O(n)<O(nlog2n)<O(n2)< O(n3)<O(2n)<O(n!)<O(nk)
常数阶 < 对数阶 < 线性阶 < 线性对数阶 < 平方阶 < 立方阶 < 指数阶 < n的阶乘 < k次方阶
空间复杂度:程序运行所占存储量也是问题规模的函数,我们以空间复杂度衡量:S(n)=O(f(n))
第二章 线性表(线性表是n个数据元素的有限序列。)
-
顺序表的表示和实现
顺序表:线性表的顺序表示是指用一组地址连续的存储单元,依次存储线性表中的数据元素。只要确定了存储线性表的起始位置(基地址),线性表中任一数据元素都可随机存取。
优点:可以直接实现元素的定位,即可以随机存取其中的任意元素
缺点:插入与删除运算的效率很低,在插入操作和删除操作时需移动大量数据。顺序存储结构的线性表的存储空间需要扩充。
1 /** 2 * SqList.cpp 3 * 顺序表的基本使用 4 * XiaMii 2020.2.10 5 */ 6 #include <stdio.h> 7 #include <stdlib.h> 8 9 #define LIST_INIT_SIZE 5 //存储空间的初始分配量 10 #define LISTINCREMENT 10 //存储空间的分配增量 11 12 typedef int elemtype; //元素类型 13 typedef struct 14 { 15 elemtype *elem; //存储空间基址 16 int length; //当前长度 17 int size; //当前分配的存储容量 18 }SqList; 19 20 /** 21 * 构造一个新的顺序表 22 **/ 23 int InitList_Sq(SqList &L){ 24 L.elem = (elemtype *)malloc(LIST_INIT_SIZE * sizeof(elemtype));//分配存储空间 25 if(!L.elem){return -1;} //存储分配失败 26 L.length = 0; //长度为0 27 L.size = LIST_INIT_SIZE; //初始存储容量 28 return 0; 29 } 30 /** 31 * 插入顺序表 32 **/ 33 int InsertList(SqList &L,elemtype e,int i){ 34 if (i<1 || i>L.length + 1){return (-1);}//插入位置不合法 35 if (L.length >= L.size){//存储空间已满 36 elemtype *newbase;//新基址 37 newbase = (elemtype *)realloc(L.elem,(L.size + LISTINCREMENT)*sizeof(elemtype));//重新分配存储空间 38 if(!newbase){return -1;}//存储分配失败 39 L.elem = newbase; 40 L.size += LISTINCREMENT; 41 } 42 elemtype *q,*p;//p为表尾位置 43 q = &(L.elem[i-1]);//插入位置 44 for (p = &(L.elem[L.length -1]); p>=q; p--){ 45 *(p+1) = *p;//插入位置及之后的元素后移 46 } 47 *q = e;//插入e 48 ++L.length;//表长加一 49 return 0; 50 } 51 /** 52 * 删除顺序表 53 **/ 54 int DeleteList(SqList &L,int i,elemtype &e){ 55 if(L.length == 0){return -1;}//为空表 56 if(i<1 || i>L.length){return -1;}//删除位置不合法 57 elemtype *q,*p; 58 q = &(L.elem[i-1]);//删除位置 59 p = &L.elem[L.length-1];//表尾位置 60 e = *q; 61 for (++q; p >= q; q++)//被删除元素之后的元素前移 62 { 63 *(q-1) = *q; 64 } 65 L.length--;//表长减一 66 return 0; 67 } 68 /** 69 * 按序号查找元素 70 **/ 71 int GetElem(SqList &L,int i,elemtype &e){ 72 if(i<1 || i>L.length){return -1;} 73 e = L.elem[i-1]; 74 return 0; 75 } 76 /** 77 * 按内容查找元素,返回值为序号 78 **/ 79 int GetElem(SqList &L,elemtype e){ 80 for (int i = 0; i < L.length; i++) 81 { 82 if(L.elem[i]==e){return (i+1);} 83 } 84 return -1; 85 } 86 /** 87 * 主函数 88 **/ 89 int main(){ 90 SqList L; 91 elemtype temp,temp2; 92 elemtype *tempdata; 93 if(-1==InitList_Sq(L)){ 94 printf("InitList_Sq error! "); 95 } 96 printf("please input 10 number: "); 97 for (int i = 0; i < 10; i++) 98 { 99 scanf("%d",&temp); 100 InsertList(L,temp,i+1); 101 } 102 DeleteList(L,3,temp2); 103 printf("%d ",temp2); 104 for (int j = 1; j < L.length+1; j++) 105 { 106 GetElem(L,j,temp2); 107 printf("%d ",temp2); 108 } 109 system("pause"); 110 return 0; 111 }
-
单链表的表示和实现
(1) 链表结点结构
用一组地址任意的存储单元存储线性表中的数据元素,用指针表示逻辑关系逻辑相邻的两元素的存储空间可以是不连续的。
以元素(数据元素的映象) + 指针(指示后继元素存储位置) = 结点 (表示数据元素 或 数据元素的存储映象)
以线性表中第一个数据元素的存储地址作为线性表的地址,称作线性表的头指针,有时为了操作方便,在第一个结点之前虚加一个“头结点”,以指向头结点的指针为链表的头指针。
优点:是一种动态结构,整个存储空间为多个链表共用。不需预先分配空间
缺点:指针占用额外存储空间。不能随机存取,查找速度慢
1 /** 2 * LinkList.cpp 3 * 单链表的基本使用和应用 4 * XiaMii 2020.2.10 5 */ 6 #include <stdio.h> 7 #include <stdlib.h> 8 9 typedef int Datatype; 10 typedef struct LNode 11 { 12 Datatype Data; 13 LNode * Next; 14 }*LinkList; 15 //LNode为链表的结点类型 16 //LinkList为指向结点的指针类型(LinkList L;表示L指向头结点(==LNode * L;)) 17 18 /** 19 * 建立头结点 20 */ 21 void InitList(LinkList &L){ 22 L = (LNode *)malloc(sizeof(LNode)); 23 if(L==NULL){return;} 24 L->Next = NULL; 25 } 26 /** 27 * 通过序号查找 28 */ 29 int GetElem(LinkList L,int i,Datatype &e){ 30 LNode* temp = L->Next; 31 int j = 1; 32 while (temp && j<i) 33 { 34 temp = temp->Next; 35 j ++; 36 } 37 if (j>i||!temp){return -1;} 38 e = temp->Data; 39 return 0; 40 } 41 /** 42 * 通过内容查找 43 */ 44 LNode* GetElem(LinkList L,Datatype e){ 45 LNode * temp = L->Next; 46 while (temp) 47 { 48 if (e != temp->Data) 49 { 50 temp = temp->Next; 51 } 52 else 53 { 54 return temp; 55 } 56 } 57 return 0; 58 } 59 /** 60 * 通过内容定位 61 */ 62 int GetLocate(LinkList L,Datatype e){ 63 LNode * temp = L->Next; 64 int i = 1; 65 while (temp) 66 { 67 if (e != temp->Data) 68 { 69 temp = temp->Next; 70 i++; 71 } 72 else 73 { 74 return i; 75 } 76 } 77 return 0; 78 } 79 /** 80 * 在单链表L第i个位置之前插入新元素e 81 */ 82 int InsertList(LinkList &L,int i,Datatype e){ 83 LNode * temp = L;//指向头结点 84 int j = 0;//计数 85 while (temp && j<i-1)//向后移到第i-1个结点 86 { 87 temp = temp->Next; 88 j++; 89 } 90 if (!temp||j>i-1){return -1;} 91 LNode * NewLNode = (LNode *)malloc(sizeof(LNode));//生成新结点 92 NewLNode->Data = e; 93 NewLNode->Next = temp->Next;//插入 94 temp->Next = NewLNode; 95 return 0; 96 } 97 /** 98 * 在带头结点的单链表L中删除第i个元素,通过e返回其值 99 */ 100 int DeleteList(LinkList &L,int i,Datatype &e){ 101 LNode * temp = L;//指向头结点 102 int j = 0;//计数 103 while (temp && j<i-1)//向后移到第i-1个结点 104 { 105 temp = temp->Next; 106 j++; 107 } 108 if (!temp->Next || j>i-1){return -1;} 109 LNode * deleteNode = temp->Next; 110 e = deleteNode->Data; 111 temp->Next = deleteNode->Next; 112 deleteNode = NULL; 113 return 0; 114 } 115 /** 116 * 逆序输入n个元素的值,建立带头结点的单链表L 117 */ 118 void CreateList(LinkList &L,int n){ 119 L = (LinkList)malloc(sizeof(LNode)); 120 L->Next = NULL; 121 LinkList p; 122 for (int i = n; i < n; i++) 123 { 124 p = (LinkList)malloc(sizeof(LNode)); 125 scanf("%d",&p->Data); 126 p->Next = L->Next; 127 L->Next = p;//插入到表头 128 } 129 } 130 /** 131 * 顺序输入n个元素的值(尾插法),建立带头结点的单链表L 132 */ 133 void CreateList_index(LinkList &L,int n){ 134 L = (LinkList)malloc(sizeof(LNode)); 135 L->Next = NULL; 136 LinkList p,q; 137 q = L; 138 for (int i = 0; i < n; i++) 139 { 140 p = (LinkList)malloc(sizeof(LNode)); 141 scanf("%d",&p->Data); 142 q->Next = p; 143 q = p;//q一直向后移 144 } 145 q->Next = NULL; 146 } 147 /** 148 * 求表长 149 */ 150 int ListLength(LinkList L){ 151 int i = 0; 152 while (L->Next) 153 { 154 L = L->Next; 155 i++; 156 } 157 return i; 158 } 159 /** 160 * 销毁链表 161 */ 162 int DestroyList(LinkList &L){ 163 LinkList p = L; 164 LinkList q; 165 while (p) 166 { 167 q = p; 168 p = p->Next; 169 q = NULL; 170 } 171 return 0; 172 } 173 /** 174 * 如果在表A中出现的元素在表B也出现,则将表A中元素删除 175 */ 176 int DelElem(LinkList A,LinkList B){ 177 int i = 0; 178 Datatype e; 179 LinkList p = B->Next; 180 while (p) 181 { 182 i = GetLocate(A,p->Data); 183 if(i==0){ 184 p = p->Next; 185 } 186 else 187 { 188 DeleteList(A,i,e); 189 if (e != p->Data) 190 { 191 return -1; 192 } 193 } 194 } 195 return 0; 196 } 197 198 int main(){ 199 LinkList A,B; 200 printf("input 10 numbers for List A: "); 201 CreateList_index(A,10); 202 printf("input 5 number for List B: "); 203 CreateList_index(B,5); 204 DelElem(A,B); 205 printf("A-B= "); 206 int n = ListLength(A); 207 for (int i = 0; i < n; i++) 208 { 209 A = A->Next; 210 printf(" %d ",A->Data); 211 } 212 DestroyList(A); 213 DestroyList(B); 214 system("pause"); 215 return 0; 216 }
-
循环双向链表的表示和实现
循环链表解决了一个很麻烦的问题:如何从中一个结点出发,访问到链表的全部结点,单向循环链表和单链表的差别仅在于,判别链表中最后一个结点的条件不再是“后继是否为空”,而是“后继是否为头结点”。
为了克服单向性这一缺点,设计出了双向链表。双向链表是在单链表的每个结点中,再设一个指向其前驱结点的指针,所以在双向链表中的结点都有两个指针域,一个指向直接后继,另一个指向直接前驱。
双向链表是单链表中扩展出来的结构,所以它的很多操作是和单链表是相同的,比如求长度、查找元素、查找元素位置等,这些操作都只要渺及一个方向的指针即可,另一指针多了也不能提供什么帮助。但是双向链表在插入和删除时,需要更改两个指针变量。
插入操作的步骤顺序是先搞定s的前驱和后继,再搞定后结点的前驱,最后解决前结点的后继。
1 /** 2 * xLinkList.cpp 3 * 双向循环链表、静态链表的基本使用和应用 4 * XiaMii 2020.2.16 5 */ 6 #include <stdio.h> 7 #include <stdlib.h> 8 9 typedef int Datatype; 10 11 //双向循环链表 12 typedef struct DCLNode 13 { 14 Datatype Data; // 数据域 15 DCLNode * Next; // 指向后继的指针域 16 DCLNode * Prior;// 指向前驱的指针域 17 }*DCLinkList; 18 /** 19 * 建立头结点 20 */ 21 void InitList(DCLinkList &L){ 22 L = (DCLNode *)malloc(sizeof(DCLNode)); 23 if(L==NULL){return;} 24 L->Next = L; //最后一个结点指向头结点 25 L->Prior = L; 26 } 27 /** 28 * 判断是否为空表 29 */ 30 bool IsEmpty(DCLinkList &L){ 31 if(L->Next==L){ 32 printf("DCLinkList为空表 "); 33 return true; 34 } 35 return false; 36 } 37 /** 38 * 求表长 39 */ 40 int ListLength(DCLinkList L){ 41 DCLNode * temp = L; 42 int i = 0; 43 while (temp->Next != L) 44 { 45 temp = temp->Next; 46 i++; 47 } 48 return i; 49 } 50 /** 51 * 在双向循环链表L第i个位置之前插入新元素e 52 */ 53 int InsertDCList(DCLinkList &L,int i,Datatype e){ 54 DCLNode *p,*s; 55 int j = 1; //计数 56 p = L->Next; //指向第一个结点 57 while (p != L){ 58 if (j<i){ 59 p = p->Next; //p为第i个结点 60 j++; 61 } 62 else{ 63 break; 64 } 65 } 66 if (j>i){return -1;}//插入位置不合理 67 s = (DCLNode *)malloc(sizeof(DCLNode));//s为待插入的结点 68 if(!s){return -1;} 69 s->Data = e; 70 s->Prior = p->Prior;//先解决s的前后继 71 s->Next = p; 72 p->Prior->Next = s;//再解决p的前结点 73 p->Prior = s;//最后解决p的前驱 74 return 0; 75 } 76 /** 77 * 在带头结点的单链表L中删除第i个元素,通过e返回其值 78 */ 79 int DeleteDCList(DCLinkList &L,int i,Datatype &e){ 80 DCLNode *p = L->Next; 81 int j=1; 82 while (p != L && j<i) 83 { 84 p = p->Next;//p为准备删除的结点 85 j++; 86 } 87 if (j!=i){return -1;}//删除位置不合理 88 p->Next->Prior = p->Prior;//修改后继结点的Prior 89 p->Prior->Next = p->Next;//修改前驱结点的Next,使得p指向的结点从链表断开 90 e = p->Data; 91 p = NULL; 92 return 0; 93 } 94 /** 95 * 主函数 96 */ 97 int main(){ 98 DCLinkList A; 99 InitList(A); 100 int a[10]={1,2,3,4,5,6,7,8,9,0}; 101 for (int i = 0; i < 10; i++) 102 { 103 InsertDCList(A,i+1,a[i]); 104 } 105 Datatype e; 106 DeleteDCList(A,3,e); 107 int n = ListLength(A); 108 for (int i = 0; i < n; i++) 109 { 110 A = A->Next; 111 printf(" %d ",A->Data); 112 } 113 system("pause"); 114 return 0; 115 }
-
静态链表的表示和实现
/** * xLinkList.cpp * 双向循环链表、静态链表的基本使用和应用 * XiaMii 2020.2.16 */ #include <stdio.h> #include <stdlib.h> typedef int Datatype; /************************************************ * 静态链表 ***********************************************/ #define ListSize 1000 //结点类型 typedef struct SLNode { Datatype data; //数据域 int cur; //指针域,指示后继结点在数组的位置 }; //静态链表类型 typedef struct SLinkList { SLNode List[ListSize]; int av; //av指向静态链表中一个未使用的位置,是备用链表的指针 }; /** * 初始化静态链表 */ int InitSList(SLinkList &L){ int i; for (i = 0; i < ListSize; i++) { L.List[i].cur = i+1;//将静态链表的游标指向下一个结点 } L.List[ListSize-1].cur = 0;//将链表最后一个结点的游标置为0 L.av = 1; } /** * 分配结点 */ int AssignNode(SLinkList &L){ int i; i = L.av;//从备用链表中取下一个结点空间 L.av = L.List[i].cur;//分配给要插入链表中的元素 return i;//返回要插入结点的位置 } /** * 回收结点 */ void FreeNode(SLinkList &L,int pos){ L.List[pos].cur = L.av;//将空闲的结点回收 L.av = pos;//备用链表指向回收的位置 } /** * 在静态链表L第i个位置之前插入新元素e */ int InsertSList(SLinkList &L,int i,Datatype e){ int k,j;//k为新结点位置,j为用于移动到对应位置的结点位置 k = L.av;//为新结点分配一个空间 L.av = L.List[k].cur;//修改备用指针 L.List[k].data = e;//赋值 j = L.List[0].cur; for (int x = 1; x < i-1; x++) { j = L.List[j].cur;//j为第i-1个结点 } L.List[k].cur = L.List[j].cur; L.List[j].cur = k; return 0; } /** * 在静态链表L中删除第i个元素,通过e返回其值 */ int DeleteSList(SLinkList &L,int i,Datatype &e){ int j,k;//k为准备删除的结点位置,j为用于移动到对应位置的结点位置 j = L.List[0].cur; for (int x = 1; x < i-1; x++) { j = L.List[j].cur;//j为第i-1个结点位置 } k = L.List[j].cur;//k为准备删除的结点位置(即i) L.List[j].cur = L.List[k].cur;//将i-1与i+1连起来 L.List[k].cur = L.av;//把i结点与当前的备用链表连起来 e = L.List[k].data;//e返回删除的值 L.av = k;//k变成最新的备用链表 return 0; } /** * 主函数 */ int main(){ SLinkList A; InitSList(A); Datatype b[10]={11,22,33,44,55,66,77,88,99,99},e; int j = 0; for (int i = 0; i < 10; i++) { InsertSList(A,i+1,b[i]); } DeleteSList(A,3,e); printf(" %d ",e); for (int i = 0; i < 15; i++) { j = A.List[j].cur; printf("%d ",A.List[j].data); } system("pause"); return 0; }
-
单链表与顺序表的比较
对于一个具有n个节点的单链表 ,在已知所指结点后插入一个新结点的时间复杂度是(O(1));在给定值为x的结点后插入一个新结点的时间复杂度是(O(n))。
在长度为N的顺序表中,插入一个新元素平均需要移动表中(N/2)个元素,删除一个元素平均需要移动((N-1)/2)个元素。
若线性表的主要操作是在最后一个元素之后插入一个元素或删除最后一个元素,则采用顺序表存储结构最节省运算时间。
线性表可用顺序表或链表存储。试问:两种存储表示各有哪些主要优缺点?
顺序表的存储效率高,存取速度快。但它的空间大小一经定义,在程序整个运行期间不会发生改变,因此,不易扩充。同时,由于在插入或删除时,为保持原有次序,平均需要移动一半(或近一半)元素,修改效率不高。
链接存储表示的存储空间一般在程序的运行过程中动态分配和释放,且只要存储器中还有空间,就不会产生存储溢出的问题。同时在插入和删除时不需要保持数据元素原来的物理顺序,只需要保持原来的逻辑顺序,因此不必移动数据,只需修改它们的链接指针,修改效率较高。但存取表中的数据元素时,只能循链顺序访问,因此存取效率不高。
若表的总数基本稳定,且很少进行插入和删除,但要求以最快的速度存取表中的元素,这时,应采用哪种存储表示?为什么?
应采用顺序存储表示。因为顺序存储表示的存取速度快,但修改效率低。若表的总数基本稳定,且很少进行插入和删除,但要求以最快的速度存取表中的元素,这时采用顺序存储表示较好。
参考:《数据结构》主编 陈锐 于聚然 合肥工业大学出版社