数据结构(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;
}
image-20220816210834399

Stack

Last-In-First-Out(LIFO)最后加入的元素最先出去;任何元素必须从顶部开始插入push(x)或移出pop();还有Top(),IsEmpty(),这些constant time O(1)

应用:function calls、recurdion;undo in an editor;Balanced Parentheses

用数组实现:image-20220816160904342image-20220816161135299

用动态数组的概念避免溢出,push发生溢出后O(n),n次的情况下求平均值O依然为1

用链表实现:

解决了溢出的问题,记得free

在尾部插入或者删除需要先遍历链表,以至于O(n)

所以在头部进行操作O(1)

image-20220816174506438 image-20220816174616712

2、反转字符或链表

反转字符串:

image-20220816182138642

反转链表: image-20220816212130952

3、check for balanced patentheses

左右括号对应。一旦遇到右括号)]}如果式子正确,总是左括号位于栈顶;如果错误,栈已空或者匹配失败

image-20220816213343896

4、中前后缀 Infix、Postfix、Prefix

中缀表达式:

前缀表达式:

后缀表达式:

image-20220816220638572 image-20220816230110853

后缀从左往右,前缀从右往左扫描push进stack;尤其注意从栈中取出数据的顺序

中缀转后缀:

数字顺序不会变,但操作符可能会变;所以根据中缀表达式中的运算符push进stack并判断与top的优先级,若top更优先或同级则先pop出top再push进新的,直到栈内没有或top优先级更低为止,扫描中缀后面的内容。最后栈非空时按照顺序全部pop

image-20220816231854002image-20220816231921166
如果有括号:左括号压入栈,遇到右括号,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保证非负)

image-20220817153708066image-20220817153809499

链表实现队列:

避免数组已满的拒绝或者新开数组导致的时间空间复杂度,也能避免不必要的数组内存占用

用front和rear两个指针储存头尾两个节点,避免插入需要遍历O(n)

image-20220817160516322image-20220817160626671

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节点

image-20220817184502947
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⌋$$

image-20220817223512613

操作花费的时间正比于树的高度,所以要尽可能减小树的高度或者平衡节点(保持高密度从而使高度减小)

Balanced binary tree平衡树,对于每个节点,左子树和右子树的高度差不大于k(大部分为1);$$diff=|h_{left}-h_{right}|$$

image-20220817224448194

使用方法:1、dynamically created nodes 动态创建节点,用指针或者引用连接起来 2、数组(用作典型的完全二叉树,按照中左右顺序储存。第i个索引的节点,它的左子节点是2i+1,右子节点为2i+2)(used for heaps)

二叉搜索树 BST

1、image-20220818154020131

每一个节点的所有左子树的节点值都比该节点的值要小(或等于),右子树上的所有节点值都比该节点的大。(对每个子树也成立)

二分搜索:已排序的一组数字,设置start和end两个位置,和最中间的数字进行比较,判断在左半边还是右半边,从而使搜索区域减半或直接与该数相等,复杂度为O(log_2_n). 在BST中先跟根比较,再判断左右子树,搜索空间减半。保持BST平衡排序避免最坏的情况。插入就是找到可以比较的节点并且左或右节点为空,会逐渐变得不平衡。

在c++中编写BST:

所有的节点将被创建new在应用程序的动态内存区(堆区)

递归*** 我的代码在xcode

image-20220818161210975 image-20220818163846202 image-20220818164020957

在内存中的储存:image-20220818173106561

除heap以外,其他都是在编译时决定,是固定大小的。heap在运行时动态增长,是唯一可以在运行时控制内存的分配和回收。

image-20220818185344898 image-20220818185514051

判断是否为二叉搜索树

image-20220821020854654

image-20220821020938869 image-20220821021015540
以上代码会重复遍历很多很多次,时间复杂度为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.

image-20220818193346062

在一些情况下,有人喜欢用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所在的位置,左右是相对固定的

对于这棵树image-20220818200525863,层次遍历: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);
}
//其他遍历在代码上的区别只是调用递归和打印顺序不同
image-20220821015803313

由栈隐式管理这一系列运行内存。递归带来的栈的增长(内存空间)取决于树的最大深度或高度,空间复杂度为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(双向链接)(用无序对表示)image-20220821220158972

全部都是有向边的图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

image-20220822192640748

假设内容长度影响小,那么顶点列表的空间复杂度为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),存在很多冗余空间,对稀疏图不利

image-20220823004416177

邻接表 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];

二叉搜索树来实现能进一步降低查询插入删除的复杂度

posted @ 2022-08-23 01:10  Meowki  阅读(336)  评论(1)    收藏  举报