1.线性与非线性结构
线性结构:一个有序数据元素的集合,线性结构指的是数据元素之间存在着**“一对一”**的线性关系的数据结构;
非线性结构:其逻辑特征是一个结点元素可能有多个直接前趋和多个直接后继。
常用的线性结构有:线性表,栈,队列,双队列,数组,串;
非线性数据结构是
1.没有对应关系的 集合结构
2.一对多的 树结构
3.多对多的 图结构或网结构
常见的非线性结构有:树(二叉树等),图(网等)。
2.树概念及结构
2.1树的概念
树是一种非线性的数据结构,它是由n(n>=0)个有限结点组成一个具有层次关系的集合。它具有以下的特点:每个结点有零个或多 个子结点;没有父结点的结点称为根结点;每一个非根结点有且只有一个父结点;除了根结点外,每个子结点可以分为多个不相交的子树 。
2.2树的相关概念
- 节点的度:一个节点含有的子树的个数称为该节点的度;A的为6
- 叶节点或终端节点:没有子节点的节点称为叶节点;B、C、H、I…等节点为叶节点
- 根结点:没有父节点的结点称为根节点;A是根结点
- 非终端节点或分支节点:有子节点的节点;A、D、E、F、G…等节点为分支节点
- 双亲节点或父节点:若一个节点含有子节点,则这个节点称为其子节点的父节点;A是B的父节点
- 孩子节点或子节点:一个节点含有的子树的根节点称为该节点的子节点;B是A的孩子节点
- 兄弟节点:具有相同父节点的节点互称为兄弟节点; 如上图:B、C是兄弟节点
- 树的度:一棵树中,最大的节点的度称为树的度; 如上图:树的度为6
- 节点的层次:从根开始定义起,根为第1层,根的子节点为第2层,以此类推;
- 树的高度或深度:树中节点的最大层次;树的高度为4
- 堂兄弟节点:父节点在同一层的节点互为堂兄弟;H、I互为兄弟节点
- 节点的祖先:从根到该节点所经分支上的所有节点;A是所有节点的祖先
- 子孙:以某节点为根的子树中任一节点都称为该节点的子孙。所有节点都是A的子孙
- 森林:由m(m>=0)棵互不相交的树的集合称为森林;
2.3树的表示——孩子兄弟表示法
typedef int DataType; struct Node { struct Node* _firstChild1; // 第一个孩子结点 struct Node* _pNextBrother; // 指向其下一个兄弟结点 DataType _data; // 结点中的数据域 };
3.二叉树的概念及结构
2.1概念
一棵二叉树是结点的一个有限集合,该集合或者为空,或者是由一个根节点加上两棵别称为左子树和右子树 的二叉树组成。
2.2特点:
- 每个结点最多有两棵子树,即二叉树不存在度大于2的结点。
- 二叉树的子树有左右之分,其子树的次序不能颠倒。
2.3特殊的二叉树
- 满二叉树:一个二叉树,如果每一个层的结点数都达到最大值,则这个二叉树就是满二叉树。如果一个二叉树的层数为K,且结点总数是(2^k) -1 ,则它就是满二叉树。
- 满二叉树是一种特殊的完全二叉树
- 完全二叉树:完全二叉树是效率很高的数据结构,完全二叉树是由满二叉树而引出来的。设二叉树的深度为h,除第 h 层外,其它各层 (1~h-1) 的结点数都达到最大个数,第 h 层所有的结点都连续集中在最左边,这就是完全二叉树。
4.二叉树的顺序结构及实现
4.1二叉树的顺序结构
顺序结构存储就是使用数组来存储,一般使用数组只适合表示完全二叉树,因为不是完全二叉树会有空间的浪费。而现实中使用中只有堆才会使用数组来存储。
二叉树顺序存储在物理上是一个数组,在逻辑上是一颗二叉树。
通常把堆(一种二叉树)使用顺序结构的数组来存储,这里的堆 != 操作系统 虚拟进程地址空间
一个是数据结构,一个是操作系统中管理内存的一块区域分段。
已知 [parent], [left] = 2 * [parent] + 1 [right] = 2 * [parent] + 2
已知[child],无论左右 [parent] = ([child] - 1) / 2
4.2堆的概念和结构
4.2.1堆的概念
堆数据结构是一种数组对象,它可以被视为一棵完全二叉树结构,所以堆也叫做二叉堆。堆是一种特殊的树形数据结构,每个结点都有一个值。作用:找数据中的最值(优先级队列)
当父结点的键值总是大于或等于任何一个子节点的键值时为最大堆。
当父结点的键值总是小于或等于任何一个子节点的键值时为最小堆。
- 堆总是一棵完全二叉树。
- 堆中某个节点的值总是不大于或不小于其父节点的值;
- 每个结点的左子树和右子树都是一个二叉堆(都是最大堆或最小堆)。
4.2.3堆的实现
向下调整算法有一个前提:左右子树必须是一个堆,才能调整。
根节点左右子树不是堆的情况::从倒数的第一个非叶子节点的 子树开始调整,一直调整到根节点的树,就可以调整成堆。
删除堆是删除堆顶的数据,将根节点跟最后一个数据一换,然后删除数组最后一个数据,再进行向下调 整算法。
//向下调整 ————> 建小堆 / 删除。 //前提是[root]左右子树已经满足堆的性质 //array 数组的开始位置 size 数组的长度 root 要调整的结点的下标 void AdjustDown(int array[], int size, int root) { while (1) { // 判断 root 是否是叶子结点,如果是,调整结束 // 因为 堆是完全二叉树,所有没有左孩子一定没有右孩子,又因为堆是顺序存储的 // 所以,找到左孩子的下标,如果左孩子的下标越界了,则没有左孩子 int left = 2 * root + 1; if (left >= size) { // 是叶子结点 return; // 本身是叶子结点, 找到左右孩子最小的一个 // 这里一定有左孩子,判断是否有右孩子 int right = 2 * root + 2; int min = left;//默认左小,右小了再换啊 if (right < size // 有右孩子 && array[right] > array[left]) // 右孩子的值小于左孩子 min = right; // 比较 array[min] array[root] if (array[root] >= array[min]) return; // 交换值 int t = array[root]; array[root] = array[min]; array[min] = t; // 需要继续向下调整,以 min 作为结点 root = min;//要调整的结点root往下走,数值变大。 } } //建堆 //如果除了根节点之外,左右两个子树都已经满足堆了,直接堆根节点进行向下调整即可 //不满足,需要相对左右两个子树建队,从上往下一次建堆 [最后一个非叶子节点开始,根] void CreateHeap(int array[], int size) { for (int i = (size - 2) / 2; i >= 0; i--) { AdjustDown(array, size, i); } } //时间复杂度 精确的是 O(n) 粗略看成 O(nlogN) //向上调整 大堆。。 插入。 array 数组 size 数组长度 child 要向上调整的结点下标 //1. 比不过 parent 2. 已经登基了 void AdjustUp(int array[], int size, int child) { while (child != 0) { //[child]是根节点就调整结束 int parent = (child - 1) / 2; if (array[parent] >= array[child]) return;//如果是,调整结束 int t = array[parent]; array[parent] = array[child]; array[child] = t; //交换 child = parent; //parent作为child往上走 } }
// 顺序表的基础的有部分规则 typedef struct Heap { int array[100]; int size; } Heap; // 初始化 void HeapInit(Heap *heap, int array[], int size) { memcpy(heap->array, array, size * sizeof(int)); heap->size = size; CreateHeap(heap->array, size); } // 插入 void HeapPush(Heap *heap, int val) { heap->array[heap->size++] = val; AdjustUp(heap->array, heap->size, heap->size - 1); } // 删除 只能删除堆顶元素,删除其他地方的无意义 void HeapPop(Heap *heap) { heap->array[0] = heap->array[heap->size - 1]; AdjustDown(heap->array, heap->size - 1, 0); heap->size--; } // 返回堆顶元素(返回最值) int HeapTop(Heap *heap) { return heap->array[0]; }
// 堆排序 可以找到最大的放最后 / 最小的放最前,但是不能足校的放最前 // 排升序,建大堆 ; 排降序,建小堆 // 因为重新调整会堆的成本更小 向下调整(O(logN)) < 建堆(O(N)) void HeapSort(int array[], int size) { CreateHeap(array, size); // n * logn // i 表示被找出的最大的数的个数 for (int i = 0; i < size - 1; i++) { // 循环 n 次 // 每次循环,会找出最大的一个数放到最后 // O(1) int t = array[0]; array[0] = array[size - i - 1]; array[size - i - 1] = t; // 进行向下调整,数据规模是 size - 1 - i AdjustDown(array, size - i - 1, 0); // logn } } //TopK问题:再海量数据中,找对大的k=10个数,建小堆(k个) //类似伪代码,实际中,size 是海量的,内存中是放不下的,需要借助文件操作 void TopK(int array[], int size, int k) { int *heap = (int *)malloc(sizeof(int)* k); for (int i = 0; i < k; i++) { heap[i] = array[i]; } // 针对 heap 建小堆,大小是k的小堆 CreateHeap(heap, k); for (int j = k; j < size; j++) { if (array[j] > heap[0]) { heap[0] = array[j]; AdjustDown(heap, k, 0); } } }
5二叉树的存储之链式存储
5.1链式结构
用链表来表示一棵二叉树,即用链来指示元素的逻辑关系。 通常的方法是链表中每个结点由三个域成,数据域和左右指针域,左右指针分别用来给出该结点左孩子和右孩子所在的链结点的存储地址 。链式结构又分为二叉链和三叉链.5.2二叉树链式结构的遍历
前序遍历(Preorder Traversal 亦称先序遍历)——访问根结点的操作发生在遍历其左右子树之前。
中序遍历(Inorder Traversal)——访问根结点的操作发生在遍历其左右子树之中(间)。
后序遍历(Postorder Traversal)——访问根结点的操作发生在遍历其左右子树之后。
前|中|后序 深度优先 栈 数组
前序|后序:方便找根 中序:方便区分左右子树
所以N、L和R 又可解释为 根、根的左子树和根的右子树。
层序 广度优先 队列 链表
层序遍历:除了先序遍历、中序遍历、后序遍历外,还可以对二叉树进行层序遍历。设二叉树的根节点所在 层数为1,层序遍历就是从所在二叉树的根节点出发,首先访问第一层的树根节点,然后从左到右访问第2层上的节点,接着是第三层的节点,以此类推,自上而下,自左至右逐层访问树的结点的过程就是层序遍历。
5.3二叉树及相关遍历的实现
typedef struct TreeNode { struct TreeNode *left;//左子树 struct TreeNode *right;//右子树 char val; } TreeNode; typedef struct Result { TreeNode *root; // 构建的树的根结点 int used; // 构建过程中用掉的 val 个数 } Result; //构建二叉树 Result CreateTree(char preorder[], int size) { //特殊情况 if (size == 0){ Result result; result.root = NULL; result.used = 0; result = 0; } char rootVal = preorder[0]; if (rootVal == '#') { Result result; result.root = NULL; result.used = 1; return result; } //递推遍历 //根 TreeNode *root = (TreeNode *)malloc(sizeof(TreeNode)); root->val = rootVal; root->left = root->right = NULL; //左子树 Result left_result = CreateTree(preorder + 1, size - 1); //右子树 Result right_result = CreateTree(preorder + 1 + left_result.used, size - 1 - left_result.used); root->left = left_result.root; root->right = right_result.root; Result result; result.root = root; result.used = 1 + left_result.used + right_result.used; return result; } // 求二叉树中所有结点的个数 // 遍历的思想 void TreeSize(TreeNode *root, int *size) { if (root == NULL) return; (*size)++; TreeSize(root->left, size); TreeSize(root->right, size); } // 递推的思想 int TreeSize2(TreeNode *root) { if (root == NULL) return 0; return 1 + TreeSize2(root->left) + TreeSize2(root->right); } // 叶子结点的个数 // 遍历的思想 void LeafSize(TreeNode *root, int *leaf_size) { if (root == NULL) return; if (root->left == NULL && root->right == NULL) (*leaf_size)++; LeafSize(root->left, leaf_size); LeafSize(root->right, leaf_size); } // 递推的思想 int LeafSize2(TreeNode *root) { if (root == NULL) return 0; if (root->left == NULL && root->right == NULL) { return 1; // 递推 return LeafSize2(root->left) + LeafSize2(root->right); } // 求第 k 层结点个数 // 用递推思想 int LevelK(TreeNode *root, int k) { if (root == NULL) return 0; // 这里肯定不是空树 if (k == 1) return 1; // 递推 return LevelK(root->left, k - 1) + LevelK(root->right, k - 1); } // 查找 TreeNode *Find(TreeNode *root, char x) { if (root == NULL) return NULL; // 根 if (root->val == x) return root; TreeNode *result = Find(root->left, x); if (result != NULL) return result; // 左子树肯定没找到 result = Find(root->right, x); if (result != NULL) return result; else return NULL; } // 二叉树的前序 | 中序 | 后序 //递归的版本 void Preorder(TreeNode *root) { if (root == NULL) return; printf("%c ", root->val); // 根 Preorder(root->left); // 左子树 Preorder(root->right); // 右子树 } void Inorder(TreeNode *root) { if (root == NULL) return; Inorder(root->left); // 左子树 printf("%c ", root->val); // 根 Inorder(root->right); // 右子树 } void Postorder(TreeNode *root) { if (root == NULL) return; Postorder(root->left); // 左子树 Postorder(root->right); // 右子树 printf("%c ", root->val); // 根 } // 非递归写法 用栈实现 void PreorderTraversalNor(TreeNode *root) { TreeNode *cur = root; // 遍历结点的指针 TreeNode *top; // 返回栈顶数据的指针 TreeNode *last = NULL; std::stack<TreeNode *> st;// <> 模板,表示栈里存的数据类型是 TreeNode * while (!st.empty() || cur != NULL) { while (cur != NULL) { // 第一次访问结点:cur printf("%c ", cur->val); st.push(cur); cur = cur->left; } top = st.top(); // 从栈里取出栈顶元素 st.pop(); if (top->right == NULL) { // 既是第二次访问,也是第三次访问 cur = top->right; st.pop(); last = top; } else { if (top->right != last) // 第二次访问 cur = top->right; else { // 第三次访问 st.pop(); last = top; } } } } void PreorderTraversalNor2(TreeNode *root) { TreeNode *cur = root; // 遍历结点的指针 TreeNode *top; // 返回栈顶数据的指针 std::stack<TreeNode *> st; while (!st.empty() || cur != NULL) { while (cur != NULL) { // 第一次访问结点:cur printf("%c ", cur->val); st.push(cur); cur = cur->left; } top = st.top(); // 从栈里取出栈顶元素 st.pop(); cur = top->right; } } void InorderTraversalNor(TreeNode *root) { TreeNode *cur = root; // 遍历结点的指针 TreeNode *top; // 返回栈顶数据的指针 TreeNode *last = NULL; std::stack<TreeNode *> st; while (!st.empty() || cur != NULL) { while (cur != NULL) { // 第一次访问结点:cur st.push(cur); cur = cur->left; } top = st.top(); // 从栈里取出栈顶元素 if (top->right == NULL) { // 既是第二次访问,也是第三次访问 printf("%c ", top->val); cur = top->right; st.pop(); last = top; } else { if (top->right != last) { // 第二次访问 printf("%c ", top->val); cur = top->right; } else { // 第三次访问 st.pop(); last = top; } } } } void InorderTraversalNor2(TreeNode *root) { TreeNode *cur = root; // 遍历结点的指针 TreeNode *top; // 返回栈顶数据的指针 std::stack<TreeNode *> st; while (!st.empty() || cur != NULL) { while (cur != NULL) { // 第一次访问结点:cur st.push(cur); cur = cur->left; } top = st.top(); // 从栈里取出栈顶元素 // 既是第二次访问,也是第三次访问 printf("%c ", top->val); cur = top->right; st.pop(); } } void PostorderTraversalNor(TreeNode *root) { TreeNode *cur = root; // 遍历结点的指针 TreeNode *top; // 返回栈顶数据的指针 TreeNode *last = NULL; std::stack<TreeNode *> st; while (!st.empty() || cur != NULL) { while (cur != NULL) { // 第一次访问结点:cur st.push(cur); cur = cur->left; } top = st.top(); // 从栈里取出栈顶元素 if (top->right == NULL) { // 既是第二次访问,也是第三次访问 printf("%c ", top->val); cur = top->right; st.pop(); last = top; } else { if (top->right != last) // 第二次访问 cur = top->right; else { // 第三次访问 printf("%c ", top->val); st.pop(); last = top; } } } }