数据结构(c/c++) 基础自学
数据结构(c/c++)
课程:from Harsha Suryanarayana mycodeschool.com
Typora笔记
1、双向联表Doubly Linked List
struct Node{
int data;
struct Node* next;
struct Node* prev;
};
- reverse look-up
- extra memory for a point to previous node
具体基本功能代码实现在list.cpp中
2、reverse a singly list
迭代
sturct Node* head;
void Reverse(){
struct Node *current,*prev,*next;
current=head;
prev=NULL;
while(current != NULL){
next =current->next;
current->next=prev;
prex=current;
current=next;
}
head =prev;
}
递归
struct Node* head;
void Reverse(struct Node* p){
if(p->next == NULL){
head=p;
return;
}
Reverse(p->next);
struct Node* q=p->next;
q->next=p;
p->next=NULL;
}
Stack
Last-In-First-Out(LIFO)最后加入的元素最先出去;任何元素必须从顶部开始插入push(x)或移出pop();还有Top(),IsEmpty(),这些constant time O(1)
应用:function calls、recurdion;undo in an editor;Balanced Parentheses
用数组实现:

用动态数组的概念避免溢出,push发生溢出后O(n),n次的情况下求平均值O依然为1
用链表实现:
解决了溢出的问题,记得free
在尾部插入或者删除需要先遍历链表,以至于O(n)
所以在头部进行操作O(1)
2、反转字符或链表
反转字符串:
反转链表: 
3、check for balanced patentheses
左右括号对应。一旦遇到右括号)]}如果式子正确,总是左括号位于栈顶;如果错误,栈已空或者匹配失败
4、中前后缀 Infix、Postfix、Prefix
中缀表达式:
前缀表达式:
后缀表达式:
后缀从左往右,前缀从右往左扫描push进stack;尤其注意从栈中取出数据的顺序
中缀转后缀:
数字顺序不会变,但操作符可能会变;所以根据中缀表达式中的运算符push进stack并判断与top的优先级,若top更优先或同级则先pop出top再push进新的,直到栈内没有或top优先级更低为止,扫描中缀后面的内容。最后栈非空时按照顺序全部pop


如果有括号:左括号压入栈,遇到右括号,pop出运算符直到遇到上一个对应的左括号,其他相同。(实现代码在xcode中 写的太不容易了呜呜)
Queues
先进先出 First-In-First-Out(FIFO)进和出是两端的操作。
EnQueue/Push(x)插入队尾; Dequeue/Pop()删除队头; front/Peek()返回队头数据; IsEmpty() 均为O(1)
可用于模拟一系列需要等待的场景,网络请求,处理器处理进程顺序;Print queue,Process scheduling, simulating wait
数组实现队列:
注意dequeue时front=rear时要全部换回-1; 随着dequeue,front左侧的内存不再使用,所以可以凭借循环数组的方式充分利用数组空间,既10大小的数组,9的下一位为0
current position=i; next position=(i+1)%n; previous position=(i+n-1)%n(加n保证非负)


链表实现队列:
避免数组已满的拒绝或者新开数组导致的时间空间复杂度,也能避免不必要的数组内存占用
用front和rear两个指针储存头尾两个节点,避免插入需要遍历O(n)


Trees
层级结构。最顶端为root,下面的节点为children,上面的是parent,有共同parent的同级的是sibling,没有children的节点为leaf(也就是最下端的)
If we can go from A to B, A is ancestor of B, B is descendent of A.
Tree is a recursive data structure.由树根root和子树sub-trees组成
有n个节点的树有n-1条边。因为除了根节点以外的所有节点刚好只有1个传入的边。
depth of x——树的节点x的深度被定义为从根节点到x节点的路径长度。每条边贡献一个单位长度。
height of x——树的节点x的高度被定义为从该节点到一个叶子节点的最长路径上的边数。树的高度被定义为根节点的高度。空节点的树高度Height of an empty tree=-1;只有一个节点即只有根的树的高度为0。
二叉树,Binary Tree,每个节点最多只有两个child节点
struct Node{
int data;
Node* left;
Node* right;
}
应用:1、storing naturally hierarchical data. eg. File system磁盘驱动器上的文件系统 2、organize data for quick search, insertion, deletion. Eg. Binary search trees 3、Trie for dictionary,用作动态的拼写检查 4、Network routing algorithm网络路由算法
Binary tree
Strict/Proper binary tree严格二叉树,每个节点有两个或者没有子节点
Complete binary tree完全二叉树,除最后一层外的其它层被完全填充,并且所有节点左对齐(也就是说最后一层的节点都靠左边,不是左节点,是这一层的节点靠左)(同一层指的是同一深度)在第i层的最大节点数量是2i(根为level 0)
Perfect Binary tree 完美二叉树,每一层都是被填满的。高度为h(根到叶子的最长路径)的树,最大节点数量等于2(h+1)-1=2^(Number Of levels)^-1
n个节点的完美二叉树高度为$$h=log_2(n+1)-1$$
完全二叉树高度为$$h=⌊log_2~~n⌋$$
操作花费的时间正比于树的高度,所以要尽可能减小树的高度或者平衡节点(保持高密度从而使高度减小)
Balanced binary tree平衡树,对于每个节点,左子树和右子树的高度差不大于k(大部分为1);$$diff=|h_{left}-h_{right}|$$
使用方法:1、dynamically created nodes 动态创建节点,用指针或者引用连接起来 2、数组(用作典型的完全二叉树,按照中左右顺序储存。第i个索引的节点,它的左子节点是2i+1,右子节点为2i+2)(used for heaps)
二叉搜索树 BST
1、
每一个节点的所有左子树的节点值都比该节点的值要小(或等于),右子树上的所有节点值都比该节点的大。(对每个子树也成立)
二分搜索:已排序的一组数字,设置start和end两个位置,和最中间的数字进行比较,判断在左半边还是右半边,从而使搜索区域减半或直接与该数相等,复杂度为O(log_2_n). 在BST中先跟根比较,再判断左右子树,搜索空间减半。保持BST平衡排序避免最坏的情况。插入就是找到可以比较的节点并且左或右节点为空,会逐渐变得不平衡。
在c++中编写BST:
所有的节点将被创建new在应用程序的动态内存区(堆区)
递归*** 我的代码在xcode
在内存中的储存:
除heap以外,其他都是在编译时决定,是固定大小的。heap在运行时动态增长,是唯一可以在运行时控制内存的分配和回收。
判断是否为二叉搜索树

以上代码会重复遍历很多很多次,时间复杂度为O(n^2),以下为解决方案:
设置左右子树的上下限,将lesser和greater两个函数的时间复杂度降为常数,对每个节点只访问一次,时间复杂度为O(n):
bool IsBinarySearchTree(Node *root,int minvalue,int maxvalue){
if(root==NULL) return true;
if(root->data>minvalue&&root->data<maxvalue&&IsBinarySearchTree(root->left,minvalue,root->data)&&IsBinarySearchTree(root->right,root->data,maxvalue))
return true;
else return false;
}
//这个函数可以写为工具函数,譬如 IsBstUtil,然后用return IsBstUtil(root,INT_MIN,INT_MAX);放在上面这个函数里方便main函数调用; INT_MIN和MAX表示的是最小/最大的整数,是宏
还有一种方法,通过中序遍历判断数字是否排序,通过返回的列表判断,或者通过跟踪上一个数据判断大小即可。
查找最大最小值
迭代/递归
//迭代
int FindMin(BstNode* root){
if(root==NULL) return -1;
BstNode* current=root;
while(current->left!=NULL){
current=current->left;
}
return root->data;
}
//递归
int FindMin(BstNode* root){
if(root==NULL) return -1;
if(root->left==NULL) return root->data;
return FindMin(root->left);
}
二叉搜索树中删除一个节点
情况一:叶子结点; 直接把上一链接改为NULL并释放这个节点的空间
情况二:待删除的节点有一个子节点; 将该节点的父节点连接到子节点,下面这个节点其他的内容仍然保持依附在这棵树上
情况三:待删除的节点有两个子节点; 1、寻找右子树的最小值(或者左子树的最大值),2、填入原节点,3、删除原来最小值的节点; 可以把情况化为前两种(这种做法要求左右严格小和大)(因为右子树的最小值总比左子树大,并且不会再有两个子节点,和删除这个节点和上两种情况一致)
代码见Xcode bstlearning中的Delete
二叉搜索树的中序后继结点 Inorder Successor in a BST
bst的中序遍历结果即为一组排序完成的数。这个问题是,给定一个bst中的值,如何找到它的中序后继,即中序遍历后下一个节点。如果通过遍历来寻找时间复杂度为O(n), 但是bst大部分的复杂度都在O(h)(平衡树中h=log2n),所以要优化策略。
Case 1:有右子树,则是右子树的最小值,即尽可能在右子树的左端(find min in right subtree or go deep to leftmost node in right subtree)
Case 2: 没有右子树**,回到父节点并查看是否已读,在没有parent指针跟踪的情况下,依然从root开始访问,会经过这个给定节点的所有祖先,中序后继将会是这条路径上最深的节点或者最深的祖先
代码依然在bstlearning中的Getsuccessor
二叉树的高度
Height of a node = Number of edges in longest path from root to a leaf node.
Height of tree = height of root.
Height of tree with one node = 0.
在一些情况下,有人喜欢用node数作为计算标准,即节点数为高度(实际这是不符合定义的),那么在root is null的情况下应当return 0。这里是指edges长度为高度,所以return -1,即空树高度为-1. O(n)
int FindHeight(Node* root){
if(root==NULL) return -1;
return max(FindHeight(root->left),FindHeight(root->right))+1;
}
二叉树的遍历(Traversal)
Breadth-first(广度优先)其中包括层次遍历(Level-ordered)
Depth-first(深度优先)访问子节点的同时访问完整的子树;其中包含前序遍历Preordered前(DLR)、Inordered中(LDR)、Postordered后(LRD)序遍历,指的是root所在的位置,左右是相对固定的
对于这棵树
,层次遍历:FDJBEGKACIH
前序DLR:FDBACEJGIHK
中序LDR:ABCDEFGHIJK
后序LRD:ACBEDHIGKJF
level-order traversal层次遍历
让节点和左右子节点按照顺序进入队列,出队的同时让其子节点入队,从而完成层次遍历;时间复杂度O(n);空间复杂度(测量数据的输入大小和内存使用增长率之间的关系)O(1)—best、类似链表的那个二叉树/ O(n)——Worst、Avg、完美二叉树
void LevelOrder(Node *root){
if(root==NULL) return;
queue<Node*> Q;
Q.push(root);
while(!Q.empty()){
Node *current=Q.front();
cout<<current->data<<" ";
if(current->left!=NULL) Q.push(current->left);
if(current->right!=NULL) Q.push(current->right);
Q.pop();
}
}
前中后序遍历
void Preorder(Node *root){
if(root==NULL)return;
cout<<root->data<<" ";
Preorder(root->left);
Preorder(root->right);
}
//其他遍历在代码上的区别只是调用递归和打印顺序不同
由栈隐式管理这一系列运行内存。递归带来的栈的增长(内存空间)取决于树的最大深度或高度,空间复杂度为O(h),worst O(n), best/avg O(log n); 每个节点都会有一个函数调用,所以时间复杂度为O(n)
Graphs
相较于树,没有连接点的规则
A graph G is an ordered pair(有序对,注意顺序) of a set V of vertices and a set E of edges. G=(V,E) 由顶点集和边集组成
表示边:有向directed(用起点和终点的有序对表示)、无向undirected(双向链接)(用无序对表示)
全部都是有向边的图Digraph,都是无向边的图an undirected graph
举例:社交网络,万维网网页链接,搜索引擎网络爬虫(图的遍历);加权图,最佳路径
|V|,|E|分别用于表示vertices和edges的数量
self-loop自环 multiedge多重边
无以上两种情况,有向图if |V|=n, then, 0≤|E|≤n(n-1); 无向图的max为n(n-1)/2
dense稠密的,边数接近定点数平方,通常使用邻接矩阵来储存; sparse稀疏的,边数少,通常使用邻接表来储存
walk一系列点和边组成的路径;path路径,用<,,,>表示,一般指simple path简单路径,不重复经过节点;trail途径表示边不重复(顶点可能重复) closed walk起终点是同一个顶点,即存在闭合途径; simple cycle闭合并且顶点和边不重复; Acyclic graph无环图,比如tree,DAG(有向无环图)
strongly connected graphs强连接,无向图中称为connected,存在从任意顶点到任意其他顶点的路径; 如果有向图非强连接,但是如果把有向边看做无向边后是强连接的,被称作弱连接weakly connected
graph representation
边列表 Edge List
假设内容长度影响小,那么顶点列表的空间复杂度为O(|V|)
边列表里的内容可以改为指针引用减少内存消耗,或者按照顶点列表的索引表示起始点(无向图为第一第二个顶点),每行的内存量就是一致的,所以空间复杂度为O(|E|)~~~最糟的情况下接近于O(|V|^2)
找到一个点的相邻节点,需要线性遍历边列表,时间复杂度为O(|E|)
判断两个给定的节点是否相连,也是线性查找,时间复杂度也为O(|E|)
邻接矩阵 Adjacency Matrix
对无向图来说是对称矩阵,默认值为1,有权重就把1修改为权重
找到一个点的相邻节点,需要先找到这个顶点对应的索引,扫描顶点列表,再扫描矩阵的行/列,时间复杂度为O(|V|)
判断两个给定的节点是否相连,给定索引的情况下,时间复杂度为O(1),直接找矩阵中对应索引的位置;(用哈希表hashtable键值对应保存,减少名字找索引的时间,从O(|V|)降为O(1))
空间复杂度为O(|V|^2),存在很多冗余空间,对稀疏图不利
邻接表 Adjacency list
使用一个指针数组来存储,空间复杂度正比于边的数量,即O(|E|);|E|<<|V|^2,无向图消耗的内存是边的两倍,有向图就是边的数量
判断两个给定的节点是否相连,扫描对应顶点的数组,线性遍历时间复杂度为O(|V|),若数组按顺序排列使用二分法遍历,时间复杂度为O(logv)
找到一个点的相邻节点,同上,一般是线性遍历,时间复杂度为O(V)(最坏情况),对于稀疏图还是占有极大优势
在这种情况下增加或删除连接,避免数组不能动态改变大小,一般使用链表实现
struct Node{
int data;
int weight;//储存权重
Node *next;
}//因此考虑权重后空间复杂度为O(|E|+|V|)
Node * A[8];
二叉搜索树来实现能进一步降低查询插入删除的复杂度
本文来自博客园,作者:Meowki,转载请先取得作者许可并注明原文链接:https://www.cnblogs.com/meowki/articles/16614775.html

浙公网安备 33010602011771号