绪论
-
数据结构被形式地定义为(K, R),其中K是数据元素的有限集,R是K上的关系的有限集。
-
在数据结构,从逻辑上可以把数据结构分成线性结构和非线性结构。
-
数据结构在计算机内存中的表示是指数据的存储结构。
-
数据项是数据的最小单元。数据元素是数据的基本单元。
-
数据处理的最小单元是数据项。
-
数据的逻辑结构和存储结构
-
逻辑结构
对数据之间关系的描述,有时就把逻辑结构简称为数据结构。
- 集合结构:集合结构的集合中任何两个数据元素之间都没有逻辑关系,组织形式松散。
- 线性结构:数据结构中线性结构指的是数据元素之间存在着“一对一”的线性关系的数据结构。
- 树状结构:树状结构是一个或多个节点的有限集合。
- 图状结构或网状结构:网络结构是指通信系统的整体设计,它为网络硬件、软件、协议、存取控制和拓扑提供标准。
-
存储结构(物理结构)
数据的存储结构是指数据的逻辑结构在计算机中的表示。数据的存储结构分为顺序存储结构和链式存储结构两种。
- 顺序存储结构:顺序存储方法它是把逻辑上相邻的结点存储在物理位置相邻的存储单元里,结点间的逻辑关系由存储单元的邻接关系来体现。
- 链式存储结构:链接存储方法它不要求逻辑上相邻的结点在物理位置上亦相邻,结点间的逻辑关系是由附加的指针字段表示的。
-
-
顺序存储结构中,数据元素都是按顺序依次存放的,并没有存储元素之间的关系。像链表,除了存储数据以外,还存储了下一个数据的指针,这才叫存储了数据元素之间的关系。
-
抽象数据类型的定义仅取决于它的一组逻辑特性,而与在计算机内部如何表示和实现无关,即不论其内部结构如何变化,只要它的数学特性不变,都不影响外部使用。
-
算法的五个特性
- 有穷性
- 确定性
- 可行性
- 有零个或多个输入
- 有一个或多个输出
-
算法的时间复杂度取决于问题的规模和待处理数据的初态。
-
数据结构由数据的逻辑结构,存储结构,运算操作三部分组成。
线性表
- 读表元计算在顺序表上只需常数时间O(1)便可实现,因此顺序表是一种随机存取的结构。
- 设一个链表最常用的操作是在末尾插入结点和删除尾结点,则选用带头结点的双循环链表最节省时间。
- 静态链表中指针表示的是数组下标。
- 线性表的常见链式存储结构
- 单链表
- 循环链表
- 双向链表
- 论述头结点,首元结点,头指针的区别。
- 算法题
-
单链表基本操作
//找出带有头结点的链表长度并返回其值 int FindListLength(LinkList Head){ int i = 0; while(Head -> next != NULL){ Head = Head -> next; i++; } return i; } //尾插法创建带有头结点的链表并返回头指针 LinkList CreateListTail(LinkList Head){ int n; printf("请输入创建的结点个数:\n"); scanf("%d", &n); Head = (LinkList) malloc (sizeof(LNode)); LinkList Tail = NULL, p; if(!Head){ printf("内存分配失败!\n"); return Head; } Head -> next = NULL; Tail = Head; int i = 0; while(i < n){ p = (LinkList) malloc (sizeof(LNode)); printf("请输入姓名:\n"); scanf("%s", p -> name); printf("请输入年龄:\n"); scanf("%d", &p -> age); p -> next = NULL; Tail -> next = p; Tail = p; i++; } Tail -> next = NULL; TraverseList(Head); return Head; } //在第i个结点前插入一个结点并返回头指针 LinkList InsertList(LinkList Head){ int i; printf("在第i结点前插入结点:\n"); scanf("%d", &i); if(i < 1 || i > FindListLength(Head)){ printf("插入位置错误,插入失败!\n"); return Head; } int j; LinkList p, s; p = Head; j = 1; while(p && j < i){ p = p -> next; ++j; } if(!p || j > i){ return ERROR; } s = (LinkList) malloc (sizeof(LNode)); printf("请输入姓名:\n"); scanf("%s", s -> name); printf("请输入年龄:\n"); scanf("%d", &s -> age); s -> next = p -> next; p -> next = s; TraverseList(Head); return Head; } //按位置删除某一结点 LinkList LocateDeleteList(LinkList Head){ int i; printf("删除第i位置上的结点:\n"); scanf("%d", &i); if(i < 1 || i > FindListLength(Head)){ printf("删除位置错误,插入失败!\n"); return Head; } int j; LinkList p, q; p = Head; j = 1; while(p && j < i){ p = p -> next; ++j; } if(!(p -> next) || j > i){ printf("删除失败!\n"); return ERROR; } q = p -> next; p -> next = q -> next; free(q); TraverseList(Head); return Head; } //按名字删除某一结点 LinkList ComDeleteList(LinkList Head){ char n[10]; printf("请输入要删除的人的姓名:"); scanf("%s", n); if(!Head){ printf("链表为空,无法删除!\n"); return Head; } LinkList p, q; p = Head; while(strcmp(n, p -> name) != 0 && p -> next){ q = p; p = p -> next; } if(strcmp(n, p -> name) == 0){ q -> next = p -> next; TraverseList(Head); return Head; }else{ printf("无该人的数据!\n"); TraverseList(Head); return Head; } }
-
单链表逆置
List Reverse( List L ) { List p = NULL, q = NULL; while(L) { q = L -> Next; L -> Next = p; p = L; L = q; } return p; }
-
两个非递减序列合成一个非递减的序列
-
List Merge( List L1, List L2 ) { List pa,pb,pc,L; L = (List)malloc(sizeof(struct Node)); pa=L1->Next; pb=L2->Next; pc = L; while(pa && pb) { if(pa->Data <= pb->Data) { pc->Next = pa; pc = pa; pa = pa->Next; } else { pc->Next = pb; pc = pb; pb = pb->Next; } } if(pa){ pc -> Next = pa; } if(pb){ pc -> Next = pb; } // pc->Next = pa ? pa : pb; L1->Next = NULL; L2->Next = NULL; return L; }
-
约瑟夫环问题(循环链表)
node *create(int n) //创建循环链表(尾插法) { node *head, *tail, *p = NULL; head = (node *) malloc (sizeof (node)); p = head; int i = 1; if(n != 0) { while(i <= n) { tail = (node *) malloc (sizeof (node)); tail -> data = i++; p -> next = tail; p = tail; } tail -> next = head -> next; //首尾相连 } free(head); return tail -> next; //返回首位 } int main() { int n, k, m, i; printf("总共有n个人:"); scanf("%d", &n); printf("从第k个人开始报:"); scanf("%d", &k); printf("循环周期m:"); scanf("%d", &m); node *p = create(n); node *q; while(p -> data != k) //确定是第k个元素 { p = p -> next; } while(p != p -> next) //只剩下一个元素 { for(i = 1; i < m - 1; i++) { p = p -> next; } printf("%d ", p -> next -> data); q = p -> next; //删除节点 p -> next = q -> next; free(q); p = p -> next; } printf("%d", p -> data); return 0; }
-
栈和队列
- 用链式方式存储的队列,在进行删除运算时头,尾指针可能都要修改。
- 循环队列的引入,目的是为了克服假溢出时移动大量元素。
- 循环队列判断空or满
- 设置flag标志
- 队列满:(Q -> rear + 1) % MAXSIZE == Q -> front;
- 循环队列也存在空间溢出问题。
- 算法题
-
括号匹配(栈)
typedef int ElemType; typedef struct sqStack{ ElemType *top; ElemType *base; int stackSize; }sqStack; void InitStack(sqStack *S){ S -> base = (ElemType*) malloc (INITSIZE * sizeof(ElemType)); if(!S -> base){ exit(0); } S -> top = S -> base; S -> stackSize = INITSIZE; } void Push(sqStack *S, ElemType e){ if(S -> top - S -> base >= S -> stackSize){ exit(0); } *(S -> top++) = e; } int stackLength(sqStack *S){ return S -> top - S -> base; } int Pop(sqStack *S, ElemType *e){ if(stackLength(S)){ *e = *(-- S -> top); return 1; } return 0; } int compare(char s[]){ sqStack S; ElemType e; int flag = 1; InitStack(&S); int i = 0; while(s[i] != '\0' && flag){ switch(s[i]){ case '(': case '[': case '{':{ Push (&S, s[i]); break; } case ')':{ if(!Pop(&S, &e) || e != '('){ flag = 0; } break; } case ']':{ if(!Pop(&S, &e) || e != '['){ flag = 0; } break; } case '}':{ if(!Pop(&S, &e) || e != '{'){ flag = 0; } break; } } i++; } if(flag && S.top == S.base && s[i] == '\0'){ return 1; }else{ return 0; } }
-
堆栈操作合法性
#include<stdio.h> #define S 1 #define X -1 int main(){ char str[100]; int n, m; int i, count; scanf("%d%d", &n, &m); while(n--){ scanf("%s", str); i=0; count=0; while(str[i]){ if(str[i]=='S'){ count++; } if(str[i]=='X'){ count--; } if(count < 0||count > m){ break; } i++; } if(count == 0){ printf("YES\n"); } else{ printf("NO\n"); } } return 0; }
-
两栈共享空间
-
堆栈模拟队列
#include <stdio.h> #include <stdlib.h> typedef struct Node{ int num; struct Node *next; }Node, *Stack; Stack Create(){ Stack s; s = (Stack) malloc (sizeof(Node)); s -> next = NULL; return s; } void Push(Stack s, int item){ //元素item压入堆栈s Stack p = (Stack) malloc (sizeof (Node)); p -> num = item; p -> next = s -> next; s -> next = p; } int Pop(Stack s){ //删除并返回s的栈顶元素 Stack q; int n; q = s -> next; n = q -> num; s -> next = q -> next; free(q); return n; } int main(){ Stack s1 = Create(); Stack s2 = Create(); int n1, n2; scanf("%d%d", &n1, &n2); //n2是大容量,n1是小容量 int temp; if(n1 > n2){ temp = n1; n1 = n2; n2 = temp; } int count1 = 0, count2 = 0; while(1){ char ch; int n; scanf("%c", &ch); if('A' == ch){ scanf("%d", &n); getchar(); if(count1 < n1){ Push(s1, n); count1++; }else{ printf("ERROR:Full\n"); } }else if('D' == ch){ if(count2 > 0){ printf("%d\n", Pop(s2)); count2--; }else{ printf("ERROR:Empty\n"); } }else if('T' == ch){ break; } if(count1 == n1 && count2 == 0){ while(count1 != 0){ Push(s2, Pop(s1)); count1--; count2++; } } } return 0; }
-
数组模拟队列
void InitQ(SqQueue &Q,int N) { Q.base = (int*)malloc(N*sizeof(int)); Q.front = Q.rear = 0; } void AddQ(SqQueue &Q, int x ) { if((Q.rear+1)%N == Q.front) { printf("Queue Full\n"); } else { Q.base[Q.rear] = x; Q.rear = (Q.rear+1)%N; } } Status DeleteQ(SqQueue &Q,int &e) { if(Q.front==Q.rear) { printf("Queue Empty\n"); return ERROR; } else { e = Q.base[Q.front]; Q.front = (Q.front+1)%N; return OK; } }
-
串
- 模式匹配 KMP算法。
- 最大的特点就是从数据元素是一个字符。
数组
- 对稀疏矩阵进行压缩存储的目的是节省存储空间。
- 稀疏矩阵的压缩方法是只存储非零元素。
树和二叉树
-
讨论树,森林和二叉树的关系,目的是为了借助二叉树上的运算方法去实现对树的一些运算。
-
利用二叉链表存储树,则根结点的右指针是空。(树转换成二叉树的时候根结点没有右孩子)
-
在二叉树结点的先序序列,中序序列,后序序列中,所有叶子结点的先后顺序完全相同。
-
二叉树的遍历只是为了在应用中找到一种线性次序。
-
哈夫曼树的结点个数不能是偶数。
-
具有n个结点的二叉树中,一共有2n个指针域,其中只有n- 1个用来指向结点的左右孩子,其余的n + 1个指针域为NULL。
-
二叉树的先序序列和中序序列相同的条件是任何结点至多只有右子女的二叉树。
-
二叉树的性质
- 在二叉树的第i层上至多有2 ^ (i - 1) 个结点(i >= 1)。
- 深度为k的二叉树至少有2 ^ (k - 1) 个结点,至多有2 ^ k - 1 个结点(k >= 1)。
- 在二叉树中,度为0的结点个数 = 度为2的结点个数 + 1。
-
树 二叉树 森林 之间的转换。
-
把一棵树转换成二叉树后,这棵二叉树的形态是唯一的,且根结点一定没有右孩子。
-
完全二叉树中,若一个结点没有左孩子,则它必是叶子结点。
-
算法题
-
求根结点个数
int LeafNode(BiTree T){ int count; if(T == NULL){ count = 0; }else if((T -> lchild == NULL) && (T -> rchild == NULL)){ count = 1; }else{ count = LeafNode(T -> lchild) + LeafNode(T -> rchild); } return count; }
-
二叉树的创建和遍历
typedef struct BiTNode{ int data; struct BiTNode *lchild, *rchild; }BiTNode, *BiTree; void CreateBiTree(BiTree *T){ int c; scanf("%d", &c); getchar(); if(0 == c){ *T = NULL; }else{ *T = (BiTree) malloc (sizeof(BiTNode)); (*T) -> data = c; CreateBiTree(&((*T) -> lchild)); CreateBiTree(&((*T) -> rchild)); } } void PreOrder(BiTree T){ if(T){ printf("%d ", T -> data); PreOrder(T -> lchild); PreOrder(T -> rchild); } } void InOrder(BiTree T){ if(T){ InOrder(T -> lchild); printf("%d ", T -> data); InOrder(T -> rchild); } } void PostOrder(BiTree T){ if(T){ PostOrder(T -> lchild); PostOrder(T -> rchild); printf("%d ", T -> data); } }
-
图
-
任何一个带权的无向连通图的最小生成树有一棵或多棵。
-
路径的定义:由顶点和相邻顶点序偶构成的边所形成的序列。
-
要连通具有n个顶点的有向图,至少需要n条边。
-
拓扑排序可以判断出一个有向图是否有环(回路)。
-
关键路径是事件结点网络中从源点到汇点的最长路径。
-
构造连通网最小生成树的两个典型算法是普里姆(prim)算法(适合稠密图)和克鲁斯卡尔(Kruskal)算法(适合稀疏图)。
-
AOV与AOE网的区别
-
AOV网:顶点表示活动,弧表示活动间的优先关系。
即如果a -> b,那么a是b的先决条件。
-
AOE网:边表示活动,是一个带权的有向无环图。
顶点表示事件,弧表示活动,权表示活动持续时间。
-
拓扑排序是AOV,关键路径是AOE。
-
-
连通分量是无向图中的极大连通子图。
-
强连通分量是有向图中的极大强连通子图。
-
若有向图G存在拓扑排序序列,则G一定不是强连通的。
-
n个顶点的连通图用邻接矩阵表示时,该矩阵至少有**2(n-1)**个非零元素。
-
具有n个顶点的无向图至多有n个连通分量。
查找
- 顺序查找法适合于存储结构为顺序存储或链式存储的线性表。
- 当采用分块查找时,数据的组织方式为:数据分为若干块,每块内数据不必有序,但块间必须有序,每块内最大(或最小)的数据组成索引块。
- 二叉查找树的查找效率与二叉树的树型有关,在呈单枝树时其查找效率最低。
- 如果要求一个线性表既能较快的查找,又能适应动态变化的要求,则可采用分块查找法。
- 散列表的平均查找长度与处理冲突方法有关,而与表的长度无关。
- 一个无序序列可以通过构造一棵二叉排序树而变成一个有序序列,构造树的过程即为对无序序列进行排序的过程。
- 哈希方法的关键是选择好的哈希函数和处理冲突的方法。一个好的哈希函数其转换地址应尽可能均匀,而且函数运算应尽可能简单。
- 平衡二叉树又称AVL树(高度平衡树),其定义是:二叉树中任意结点左子树高度与右子树高度差的绝对值小于等于1。
- 在哈希函数H(key)= key % p中,p值最好取小于等于表长的最大素数。
- 直接定址法构造的哈希函数肯定不会发生冲突。
- 动态查找表和静态查找表的重要区别在于前者包含有插入和删除运算,而后者不包含这两种运算。
- 在散列存储中,装填因子的值越大,则存取元素时发生冲突的可能性就越大。
- 假定有K个关键字互为同义词,若用线性探测再散列法把这K个关键字存入散列表中,至少要进行k ( k - 1 ) / 2次探测。
排序
-
比较次数与序列初态无关的算法是归并排序,简单选择排序。
-
不稳定的排序算法是快速排序,简单选择排序,堆排序。
-
时间复杂度O(nlogn):堆排序, 归并排序,快速排序。
-
归并排序在一趟结束后不一定能选出一个元素放在其最终位置上。快速,冒泡,堆排序可以。
-
在待排序数据已有序时,花费时间反而最多的是快速排序。
-
就平均性能而言,目前最好的内排序方法是快速排序法。
-
占用辅助空间最多的是归并排序。
-
按照排序过程涉及的存储设备不同,排序可分为内部排序和外部排序。
-
直接插入排序用哨兵的作用是免去查找过程中每一步都要检测整个表是否查找完毕,提高了查找效率。
-
一个无序序列可以通过构造一棵二叉排序树而变成一个有序序列,构造树的过程即为对无序序列进行排序的过程。
-
哈希方法的关键是选择好的哈希函数和处理冲突的方法。一个好的哈希函数其转换地址应尽可能均匀,而且函数运算应尽可能简单。
-
平衡二叉树又称AVL树(高度平衡树),其定义是:二叉树中任意结点左子树高度与右子树高度差的绝对值小于等于1。
-
在哈希函数H(key)= key % p中,p值最好取小于等于表长的最大素数。
-
直接定址法构造的哈希函数肯定不会发生冲突。
-
动态查找表和静态查找表的重要区别在于前者包含有插入和删除运算,而后者不包含这两种运算。
-
在散列存储中,装填因子的值越大,则存取元素时发生冲突的可能性就越大。
-
假定有K个关键字互为同义词,若用线性探测再散列法把这K个关键字存入散列表中,至少要进行k ( k - 1 ) / 2次探测。
排序
- 比较次数与序列初态无关的算法是归并排序,简单选择排序。
- 不稳定的排序算法是快速排序,简单选择排序,堆排序。
- 归并排序在一趟结束后不一定能选出一个元素放在其最终位置上。快速,冒泡,堆排序可以。
- 在待排序数据已有序时,花费时间反而最多的是快速排序。
- 就平均性能而言,目前最好的内排序方法是快速排序法。
- 占用辅助空间最多的是归并排序。
- 按照排序过程涉及的存储设备不同,排序可分为内部排序和外部排序。
- 直接插入排序用哨兵的作用是免去查找过程中每一步都要检测整个表是否查找完毕,提高了查找效率。