数据结构 1.数据类型 【原子类型】: 是不可以在分解的基本数据类型,包含整型,浮点型,字符型等。 【结构类型】: 由若干类型组合而成,是可以再分解的,例如,整型数组就是由若干整型数据组 成的 2.抽象数据类型 指一个数学模型以及定义在该模型上的一组操作 比如使用到坐标,xyz 坐标 3.数据结构 类型 集合结构: 一群数据归类到一个集合中,集合有包含关系,交集,并集,包含等等 线性结构 元素之间的关系是一对一的 常用的线性结构有: 线性表、栈、队列、双队列、数组、串。 树形结构 元素是一对多的层级关系 常见的树形结构: 二叉树、B树、哈夫曼树、红黑树等 图形结构 数据元素之间的关系是多对多的 常见的图形结构:邻近矩阵、邻接表。 顺序存储结构 把数据元素存放在地址连续的存储单元里,内存地址也是连续的 数组: 它的元素是一个接一个,在内存空间中的地址也是连续的 栈: 连续的存储结构 链式存储结构 数据元素放在任意的存储单元里,可以连续也可以不连续 堆: 既可以非连续存储分配,也可以分配连续地址 4.算法是什么? 算法就是解决特定问题求解步骤的描述,在计算机中表现为指令的有限序列,并 且每个指令表示一个或者多个操作 特性: 输入输出 有输入有输出 有穷性 计算过程不是无穷的或死循环的 确定性 确定的步骤有确定的结果,不是未知结果 可行性 每一步都能通过执行有限次数完成 设计要求: 正确性 正常数据有正常唯一结果,非正常数据有对应返回 可读性 尽量便于阅读,理解和交流 健壮性 考虑边界性,当输入数据不合法时,算法也能做出相关处理,而不是产生异常和 莫名其妙的结果 时间效率高和存储量低 时间和空间的最优解 时间复杂度 常数阶 有限常数次都是O(1)时间复杂度 比如 1000,10000 都是 线性阶 for (int i = 0; i < n; i++) { // 时间复杂度为O(n)的操作 } 对较大数n的遍历时间复杂度O(n) 对数阶 int i = 1; while (i < n) { i = i * 2; } 在循环体内,i每次都乘以2倍,即为2^x = n,可以得出次数x为以2为低n的对 数x = log2n。根据推导大O阶的方法,去除最高阶项的常数,即为O(logn)。 平方阶 for (int i = 0; i < n; i++) { for (int j = 0; j < n; j++) { // 时间复杂度为O(n^2)的操作 } } 循环嵌套n*n,时间复杂度为O(n^2) 对比: O(1) < O(log n) < O(n) < O(nlog n) < O(n^2) < O(n^3) < O(2^n) < O(n!) < O(n^n) 空间复杂 和时间复杂度类似,只不过,空间复杂是计算存储空间的遍历 ,也是O(x) 主要是对数据进行操作的辅助空间 线性表 顺序表 Array,list 都是顺序表,通过下表获取或者删除数据 单链表 //定义结点 typedef struct KNodeInfo{ KElementType data; struct KNodeInfo *next; }Node; 由数据和next指针组成 结点p数据: p->data 结点p下一个接到: p->next 删除结点q //q指向要删除的结点 q = p->next; //将q的后继赋值给p的后继 p->next = q->next; //将q结点中的数据赋值给e *e = q->data; //释放内存 free(q); 插入结点s //生成新结点s s = (KLinkList)malloc(sizeof(Node)); //将e赋值给s的数值域 s->data = e; //将p的后继结点赋值给s的后继 s->next = p->next; //将s赋值给p的后继 p->next = s; 遍历单链表 while (p) { p = p->next; // printf } 清空单链表 //指向第一个结点 p = (*head)->next; while (p) { //遍历删除每个结点,并释放内存 q = p->next; free(p); p = q; } 单向循环链表 空循环链表 只有一个结点,它的next指针指向自己 判断是否单向循环链表? 双指针,一个定位标记,一个遍历,最终会相等 遍历循环链表 // 2.遍历循环链表 void traverseCircleList(LinkList L) { //判断链表是否为空 if(!L) { printf("链表为空!\n"); return; } LinkList temp; temp = L; do { printf("%6d", temp->data); temp = temp->next; }while (temp != L) ; printf("\n"); } 插入和删除同单向链表 双向链表 //定义结点 typedef struct KNode{ KElementType data; //数据 struct KNode *prior; //前驱指针 struct KNode *next; //后继指针 }Node; 为什么要双向? 如果要删除一个指定结点q,需要将q上一个结点指向下一个结点, 单向链表可以获取next,但是前一个结点无法获取,1 要么提前记录了 2要么从 表头开始遍历 。 这样成本太高,如果新增一个prioe 前指针,那么问题就简单 操作: 同单向链表,只是多了一个前指针设置 顺序栈 先进后出 FILO //顺序栈结构 typedef struct KSqueueStack{ KStackElementType data[MAXSIZE]; int top; //用于栈顶指针 }SqStack; 栈置空 KStatus clearStack(SqStack *S) { S->top = -1; } 顺序栈获取长度 //4. 获取栈长度 int getLength(SqStack S) { return S.top + 1; } 栈顶元素 //栈空,则返回错误 if (S.top == -1) return ERROR; *e = S.data[S.top]; 入栈 //6. 压栈 KStatus push(SqStack *S, KStackElementType e) { //判断是否 栈满 if (S->top == MAXSIZE -1) return ERROR; S->data[++(S->top)] = e; return OK; } ========解释==================== //1. 栈顶指针+1; //2. 将新插入的元素赋值给栈顶空间 //S->top ++; //S->data[S->top] = e; 出栈 //判断是否栈空 if(S->top == -1) return ERROR; *e = S->data[S->top--]; ======解释== //1. 将要删除的栈顶元素赋值给e //2. 栈顶指针--; //*e = S->data[S->top]; //S->top--; 链式栈 链式栈的栈顶不再是简单int 记录长度,而是增加结点,形成链 结点结构: //链栈结点 typedef struct KStackNode { KStackElementType data; //结点数据 struct KStackNode *next; //指向下一个结点的指针 }StackNode, *LinkStackPtr; 链栈结构: //链栈结构 typedef struct KLinkStack { LinkStackPtr top; //栈顶结点 int count; //栈大小 }LinkStack; 压栈 //6. 压栈 KStatus push(LinkStack *S, KStackElementType e) { //1. 创建一个新结点, LinkStackPtr newNode = (LinkStackPtr)malloc(sizeof(StackNode)); //2. 赋值给新结点 newNode->data = e; //3. 插入新结点到栈顶结点后面 //3.1 把当前的栈顶元素的结点指针指向直接后继结点 newNode->next = S->top; //3.2 将新结点赋值给栈顶指针 S->top = newNode; //栈大小+1 S->count++; return OK; } 出栈 //7. 出栈 KStatus pop(LinkStack *S, KStackElementType *e) { LinkStackPtr p; if (isEmpty(*S)) return ERROR; //1. 将栈顶元素赋值给*e *e = S->top->data; //2. 将栈顶结点赋值给p p = S->top; //3. 使得栈顶指针下移一位, 指向后一结点 S->top = S->top->next; //4. 释放p结点 free(p); //栈大小减1 S->count--; return OK; } 队列 是一种先进先出(First In First Out)的线性表,也就是FIFO 通常,称进数据的一端为 "队尾",出数据的一端为 "队头",称入队和出队 单向队列 注意 底层使用的是数组 需预先申请一块足够大的内存空间初始化顺序队列 需要定义两个指针(top 和 rear)分别用于指向顺序队列中的队头元素和队尾元 空队列:双指针都指向同一个地址 插入数据后:头指针指向第一个数据,尾指针指向最后一个数据后面那个空位置 (不是最后一个数据) 删除数据(出队列)top指针是移动,这样连续地址上前后端都会出现空缺 如果直接不断插入就会溢出,因为头指针前有空位没有利用 如果出队列后,全部移动,性能不佳 下面介绍循环队列 循环队列 前面介绍出队列后,top指针前有连续区域不能利用,如果是循环队列,那就么 有这个问题了。 /* 循环队列的顺序存储结构 */ typedef struct KQueue { KQueueElementType data[MAXSIZE]; int front; /* 头指针 */ int rear; /* 尾指针,若队列不空,指向队列尾元素的下一个位置 */ }KQueue; Q.front 前针 , Q.rear 后指针 队列为空 Q.front == Q.rear 队列已满 Q.front = Q.rear + 1表示堆满 也就是当剩余最后一个空间当时候,不再插入数据,【插满了就无法判断是空还 是满队列】 入队 //判断队列是否满了 if (isFull(*Q)) { return ERROR; } //将元素e赋值给队尾 Q->data[Q->rear] = e; //rear指针向后移动一位,若到最后则转到数组头部 Q->rear = (Q->rear + 1) % MAXSIZE; return OK; 出队 if (isEmpty(*Q)) return ERROR; //从队头取出元素赋值给e *e = Q->data[Q->front]; //front指针向后移动一位,删除对头元素 Q->front = (Q->front + 1) % MAXSIZE; 物理上,一般基于线性数据结构 (如数组、向量、链表等)的一种数据结构 比如:【 1,2,3,4,5 】 逻辑上,是二叉树结构 是基于 完全二叉树 和 搜索二叉树 之间的排序 搜索二叉树 下, 结点 2 左孩子 3,右孩子是4 。 堆的结点2 ,它左可以3,也可以4,只要比孩子结点小就行(小根堆) 内存空间 堆的存储空间可以是非连续的,需要用户管理,适合链表结构,不像栈是完全连 续空间 根据根节点是最大值还是最小 来划分 大根堆: 结点都大于孩子结点,根结点是最大的 小根堆:根结点是最小的,所有结点都小于它的孩子结点 堆插入数据 为了满足大小根堆 规则,插入的数据不是简单的插入首尾 元素上移这个操作和这个树的深度有关系因此需要O(log n)的时间复杂度 大根堆 max-heap 尾部插入数据 判断比父结点大,就交换,继续比较父结点,直到比父结点小,或者自己成为根 结点 小根堆 min-heap 尾部插入 判断比父结点大就交换,直到根结点 删除数据 删除结点后,都需要重新判断父子结点大大小,进行交换 数组 和 堆二叉树 转换 搜索 比如大根堆:搜索8,如果搜索到某个结点是7,那么7及其子节点不需要再搜索 堆的搜索效率不是很高 效率 堆 不管是插入,删除,还是搜索,都不是很高效,因此Swift才推荐用户对使用 结构体,因为结构体存储在栈上,效率高