当前位置: 首页 > news >正文

二叉树链式存储结构

目录

1.二叉树链式存储结构

2.二叉树的遍历

2.1 前、中、后序遍历

2.2 层序遍历

3.二叉树的其他递归问题

3.1 二叉树的结点个数

​3.2 二叉树的叶子结点个数

3.3 二叉树第k层结点个数

3.4 二叉树的深度

3.5 二叉树查找

3.6 二叉树销毁

4.二叉树的基础OJ题

4.1 单值二叉树

4.2 检查两棵树是否相同

4.3 对称二叉树

4.4 另一棵树的子树

4.5 二叉树翻转

4.6 二叉树的前序遍历

4.7 二叉树的中序遍历

4.8 二叉树的后序遍历

4.9 二叉树的创建


1.二叉树链式存储结构

在此之前我们先复习一下二叉树的概念:

二叉树是:

  1. 空树

  2. 非空:根结点,根结点的左子树,根结点的右子树组成

从二叉树的定义中,我们可以发现二叉树是递归式定义的(一颗二叉树由根,左子树和右子树组成,左子树又是由根,左子树,右子树组成...依次递归),因此后面有关二叉树的操作都是根据递归实现的。

二叉树的链式存储结构是指,用链表来表示一棵二叉树,即用链来指示元素的逻辑关系。通常的方法是链表中每个结点由三个域组成,数据域和左右指针域,左右指针分别用来给出该结点左孩子和右孩子所在的链结点的存储地址。链式结构又分为二叉链和三叉链,当前我们学习中一般都是二叉链。高阶二叉树(AVL树、红黑树)会用到三叉链。 ​

typedef char BTDataType;
​
//二叉链表
typedef struct BinaryTreeNode
{BTDataType data;struct BinaryTreeNode* left;struct BinaryTreeNode* right;
}BTNode;
​
//三叉链表
typedef struct BinaryTreeNode
{BTDataType data;struct BinaryTreeNode* parent;struct BinaryTreeNode* left;struct BinaryTreeNode* right;
}BTNode;

说明:由于普通二叉树存储数据没有任何意义,不如直接存储在线性表中,所以我们不关心他的增删查改,只关注他的递归遍历结构(高阶二叉树,如二叉树搜索树、AVL树、红黑树研究增删查改)。

2.二叉树的遍历

2.1 前、中、后序遍历

学习二叉树结构,最简单的方式就是遍历。所谓二叉树遍历(Traversal)是按照某种特定的规则,依次对二叉树中的节点进行相应的操作,并且每个节点只操作一次。访问结点所做的操作依赖于具体的应用问题。遍历是二叉树上最重要的运算之一,也是二叉树上进行其它运算的基础。

按照规则,二叉树的遍历有:前序/中序/后序的递归结构遍历:

  1. 前序遍历(Preorder Traversal亦称先序遍历)――访问根结点的操作发生在遍历其左右子树之前。

  2. 中序遍历(Inorder Traversal)——访问根结点的操作发生在遍历其左右子树之中(间)。

  3. 后序遍历(Postorder Traversal)——访问根结点的操作发生在遍历其左右子树之后。

由于被访问的结点必是某子树的根,所以N(Node)、L(Left subtree)和R(Right subtree)又可解释为根、根的左子树和根的右子树。NLR、LNR和LRN分别又称为先根遍历、中根遍历和后根遍历。

代码实现:

void PreOrder(BTNode* root)
{if (root == NULL){printf("NULL ");return;}
​printf("%c ", root->data);PreOrder(root->left);PreOrder(root->right);
}
​
void InOrder(BTNode* root)
{if (root == NULL){printf("NULL ");return;}
​InOrder(root->left);printf("%c ", root->data);InOrder(root->right);
}
​
void PostOrder(BTNode* root)
{if (root == NULL){printf("NULL ");return;}
​PostOrder(root->left);PostOrder(root->right);printf("%c ", root->data);
}

这三个递归遍历的代码看似简单,但是对于初学者的人来说并不是特别好理解他其中递归层次的变化。

下面主要分析前序递归遍历,中序与后序类似。

前根遍历递归图解:

前根遍历代码的递归展开图:

对于初学二叉树递归的人来说,画代码的递归展开图是最好的理解方式,不理解的代码画一画展开图就会一目了然。

2.2 层序遍历

除了先序遍历、中序遍历、后序遍历外,还可以对二叉树进行层序遍历。设二叉树的根节点所在层数为1,层序遍历就是从所在二叉树的根节点出发,首先访问第一层的树根节点,然后从左到右访问第2层上的节点,接着是第三层的节点,以此类推,自上而下,自左至右逐层访问树的结点的过程就是层序遍历。

如何进行层次遍历?这就要用到前面所学的队列这种数据结构了,详细见:栈和队列实现

接下来我们说说队列实现层次遍历算法设计思路:

使用一个队列

  • 第一步:将根结点进队;

  • 第二步:控制一个循环,队列不空时,取队头结点,Pop掉这个队头结点并访问他:

若他有左孩子结点,将左孩子结点进队;

若他有右孩子结点,将右孩子结点进队;

每次循环访问队头元素时,都是访问当层结点,而当层结点的左右孩子都是下一层结点,这些孩子结点进队列后等待访问,这样就可以做到层次遍历。

下面就是层次遍历的代码实现;

需要注意的一点是,队列存储的是树的结点,还是结点的val值呢?很明显是树的结点,因为我们还要将节点的左右孩子入队,这就需要访问节点的左右孩子。

那么队列存储的数据类型为结点的数据类型,结点的数据类型是我们自定义的数据类型BTNode*,但是编译器只认识内置数据类型,我们有两种方式来解决:1.进行前置声明 2.头文件声明位置放在BTNode类型声明的下面,如:

struct BinaryTreeNode;
typedef struct BinaryTreeNode* QDataType;
​
typedef struct QueueNode
{QDataType data; struct QueueNode* next;
}QueueNode;
void LevelOrder(BTNode* root)
{Queue q;QueueInit(&q);
​//根节点入队if(root) QueuePush(&q, root);
​while (!QueueEmpty(&q)){//取队头结点(当层结点),访问并PopBTNode* front = QueueFront(&q);QueuePop(&q);printf("%d ", front->val);
​//左右孩子不为空,入队列if (front->left)QueuePush(&q, front->left);if (front->right)QueuePush(&q, front->right);}printf("\n");  
​QueueDestory(&q);
}

下面我们根据这个层次遍历来实际解决一个衍生问题:判断一个树是否为完全二叉树

我们就可以利用测那层次遍历来解决这个问题:

我们来看一下完全二叉树和非完全二叉树他们在层次遍历时队列变化有什么区别

(这里的层次遍历我们带上空结点,图中空结点用#表示)

可以大概了解出:

完全二叉树层次遍历遇到空结点后,后面全是空结点。

非完全二叉树遇到空结点后,后面有非空结点。

我们根据这点来实现代码:

bool IsComplete(BTNode* root)
{Queue q;QueueInit(&q);
​if (root)QueuePush(&q, root);
​while (!QueueEmpty(&q)){BTNode* front = QueueFront(&q);QueuePop(&q);
​//front为空,说明遇到空结点,breakif (!front)break;
​//这里同层序遍历不同的是,空结点也要入队QueuePush(&q, front->left); QueuePush(&q, front->right); }
​while(!QueueEmpty(&q)){BTNode* front = QueueFront(&q);QueuePop(&q);
​//说明空结点后有非空结点,返回falseif (front){QueueDestory(&q);return false;}}//空结点后全是空结点,返回trueQueueDestory(&q); return true;
}

注:代码相比于层序遍历不同的是,空结点也是要入队的。其次是要注意队列销毁的时机。

3.二叉树的其他递归问题


二叉树的其他递归问题,主要有以下这些:

  1. 二叉树的结点个数

  2. 二叉树的叶子结点个数

  3. 二叉树第k层结点个数

  4. 二叉树的深度

  5. 二叉树查找

这些问题相对遍历递归,对递归的理解又有了更进一步的的要求。

关于递归,我们先看定义:

递归是一种算法或函数调用自身的方式。在递归的过程中,问题被分解为更小的子问题,直到达到基本情况(终止条件),然后通过将子问题的解合并成原始问题的解来解决问题。

递归的实现通常包括两个部分:

  1. 递归终止条件:确定何时停止递归,防止无限循环。这通常是在问题达到某种简单形式或特定值时。

  2. 递归调用:在解决问题的过程中,将问题分解为更小的子问题,并通过调用自身来解决这些子问题。

递归算法的优点是可以简化问题的解决方式,使代码更具可读性和简洁性。但是,递归算法也有一些潜在的问题:

  • 递归算法可能会导致性能问题,因为每次递归调用都需要创建新的函数调用栈。

  • 如果递归调用的层次太深,可能会导致栈溢出的问题。

  • 递归算法的实现可能会比迭代算法更复杂。

递归的一些常见应用包括:树的遍历,排序算法(如归并排序、快速排序)、动态规划和回溯等。

在使用递归时,需要注意选择合适的终止条件和避免无限循环。此外,递归算法也可以通过迭代的方式重新实现,以提高性能和减少内存消耗。

总的来说,递归就是将一个问题分解成更小的子问题,一直分解一直分解,直到这个子问题能够被直接解决,这种子问题也就是最小子问题,也叫做递归的终止条件。

对于绝大多数的二叉树递归问题,最小子问题一般都是空树。(并不是全部,只是占多数)


3.1 二叉树的结点个数

这个问题也可以不用递归来解决,我们可以用一个全局变量或者局部静态变量来记录结点数count,然后先根遍历对count进行++,或者传count计数变量的指针也是可以的,但是这些方法明显都比较挫,就比如局部静态变量计数,这种变量只能记录一次节点总数,计数完之后要置空,就非常麻烦。所以我们讲解一下递归解法:

递归终止:root == NULL ,结点个数为0;

递归调用:root != NULL,结点个数 == 1(根结点) + 左子树结点个数 + 右子树结点个数

左右子树的结点个数可以分解成子问题。

int BinaryTreeSize(BTNode* root)
{return root == NULL ? 0 : BinaryTreeSize(root->left) + BinaryTreeSize(root->right) + 1;
}

我们来画他的递归展开图,便于理解:

3.2 二叉树的叶子结点个数

叶子结点:没有左右孩子,root->left == NULL && root->right ==NULL

递归终止:

  1. root == NULL ,没有叶子结点;

  2. root != NULL, root->left == NULL && root->right ==NULL:这棵树就是一个叶子结点;

递归调用:

root != NULL 并且 不是叶子结点,叶子结点个数 == 左子树叶子结点个数 + 右子树叶子结点个数;

左右子树的叶子节点数可以分解成子问题。

int BinaryTreeLeafSize(BTNode* root)
{if (root == NULL){return 0;}
​if (root->left == NULL && root->right == NULL){return 1;}
​return BinaryTreeLeafSize(root->left) + BinaryTreeLeafSize(root->right);
}

3.3 二叉树第k层结点个数

递归终止:

  1. root == NULL : 空树,第k层结点个数为0

  2. root != NULL && k == 1 :第一层为根结点,结点个数为1

递归调用:

root !=NULL && k>1 : 第k层结点个数 == 左子树第k - 1 层结点个数 + 右子树第k - 1 层结点个数

左右子树第k - 1 层结点个数可以分解成子问题

int BinaryTreeLevelKSize(BTNode* root,int k)
{assert(k >= 1);
​if (root == NULL){return 0;}
​if (k == 1){return 1;}
​return BinaryTreeLevelKSize(root->left, k - 1) + BinaryTreeLevelKSize(root->right, k - 1);
​
}

3.4 二叉树的深度

递归终止:

root == NULL : 深度为0

递归调用:

root != NULL : 深度 == 左右子树中深度较大的 + 1

左右子树深度可以分解成子问题

 
int BinaryTreeDepth(BTNode* root)
{if (root == NULL){return 0;}else{//不要用这种方式,BinaryTreeDepth反复调用,表达式比较时候调用,后面又调用一次,对栈帧的消耗比较大//return BinaryTreeDepth(root->left) > BinaryTreeDepth(root->right)//	? BinaryTreeDepth(root->left) + 1//	: BinaryTreeDepth(root->right) + 1;//这里最好用一个变量保存起来int leftDepth = BinaryTreeDepth(root->left);int rightDepth = BinaryTreeDepth(root->right);return leftDepth > rightDepth ? leftDepth + 1 : rightDepth + 1;}
}

注意:就像代码中所写的那样,我们要避免递归函数的重复调用,毕竟每一次递归函数的调用都是对函数栈帧的一个大消耗,所以这个问题要注意。

3.5 二叉树查找

递归终止:

  1. root == NULL : 空树,查找失败

  2. root != NULL && root->val == x : 查找成功

递归调用:

root != NULL && root->val != x : 去左子树和右子树中查找

左右子树的查找可以分解成子问题

BTNode* BinaryTreeFind(BTNode* root, BTDataType x)
{if (root == NULL){return NULL;}if (root->data == x){return root;}//这里和上一个函数是一个道理,要保存结果,不然函数反复调用//if (BinaryTreeFind(root->left,x))//{//	return BinaryTreeFind(root->left, x);//}//if (BinaryTreeFind(root->right, x))//{//	return BinaryTreeFind(root->right, x);//}//不建议这样写,因为如果在左子树中找到了,右子树中就白找了//BTNode* leftRet = BinaryTreeFind(root->left, x);//BTNode* rightRet = BinaryTreeFind(root->right, x);//if (leftRet)//{//	return leftRet;//}//else//{//	return rightRet;//}//先找左子树,左子树找到了直接返回,不用在右子树里面找BTNode* leftRet = BinaryTreeFind(root->left, x);if (leftRet)return leftRet;BTNode* rightRet = BinaryTreeFind(root->right, x);if (rightRet)return rightRet;return NULL;
}

注意:

这种代码有两个注意点,一个就是上面所说的函数重复调用问题,还有一个就是左右子树递归调用的顺序问题,假如左子树找到了,我们就不需要在右子树里面找了。

3.6 二叉树销毁

二叉树销毁最好的方式是后序遍历,避免找不到左右子树,根放在后面销毁。(当然也可以不用后序,就像链表头删一样,先用个变量保存左右子树再销毁根也是可行的)。

void BinaryTreeDestory(BTNode* root)
{if (!root)return;BinaryTreeDestory(root->left);BinaryTreeDestory(root->right);free(root);
}

4.二叉树的基础OJ题

4.1 单值二叉树

bool isUnivalTree(struct TreeNode* root){}

检查一棵树root是否为单值二叉树,也就是判断根结点的val是否和左右子树根结点的val相等,然后再分解成左右子树问题,绝大多数人会先想到用这种方法来判断:

if(root->val == root->left->val && root->val == root->right->val)return true;

但是这个代码真的能解决问题吗?显然不可以!这个代码有两个问题:

  1. 根结点的val和左右子树根结点的val相等,就是单值二叉树了吗?就能直接返回true了吗?显然不能!

  2. 能一定保证root != NULL?如果root为NULL,是不是编译出错?

递归终止:

  1. root == NULL :空树,返回true(说明递归到底了)

  2. root != NULL 并且 root->val != root->left->val || root->val != root->right->val:不是单值,返回false(注意:root->left和root->right不能为空)

递归调用:

root != NULL 并且 root->val == root->left->val && root->val == root->right->val :根结点符合单值条件,再判断左右子树是否为单值

左右子树的单值问题可以分解为子问题

bool isUnivalTree(struct TreeNode* root)
{if(root == NULL)return true;//左子树不为空且根的val和左子树的根的val不同if(root->left && root->val != root->left->val)return false;//右子树不为空且根的val和右子树的根的val不同if(root->right && root->val != root->right->val)return false;  return isUnivalTree(root->left) && isUnivalTree(root->right);
}

4.2 检查两棵树是否相同

bool isSameTree(struct TreeNode* p, struct TreeNode* q){}

检查两棵树是否相同,从递归思想来看,可以先判断根结点是否相等,在递归判断左右子树。

但是这种想法是错误的!不相等的两棵树,不单单是结点的val不相等,还有可能是树的形状不同,也就是两边结点一个为空一个不为空。

树的形状不同
树的形状不同
结点的val不同
结点val不同

递归终止:

p == NULL && q == NULL:两个结点都为空,返回true

p == NULL || q == NULL:一个结点为空,一个结点不为空,返回false

p != NULL && q != NULL, p->val != q->val:两个结点都不为空,并且结点的值不同,返回false

递归调用:

p != NULL && q != NULL, p->val == q->val:这两个结点都不为空,并且结点的值相同,递归判断他们左右子树是否相等

左右子树的相等问题可以分解成子问题

//判断两棵树是否相等,先判断根是否相等,再递归判断左右子树是否相等
bool isSameTree(struct TreeNode* p, struct TreeNode* q)
{/*if(p->val == q->val)return true;*///上面这种代码不能解决问题,首先只有根相等并不能断定他们相等,其次我们并不知道p或者q是否为NULL//但是根结点不相等我们能直接判断两棵树不相等,其次我们要讨论结点为NULL的情况//两个根结点都为NULLif(p == NULL && q == NULL)return true;//两个根结点一个为NULL,一个不为NULL,两棵树必定不相等//(两个都为NULL也能进if语句里面,但是两个都为NULL一定先进上面的if语句里面)if(p == NULL || q == NULL)return false;//两个根结点都不为NULL,并且val值不相等,两棵树必定不相等if(p->val != q->val)return false;//两个根结点都不为NULL,val值相等,说明根相等,在递归判断左右子树是否相等return isSameTree(p->left, q->left) && isSameTree(p->right, q->right);
}

4.3 对称二叉树

bool isSymmetric(struct TreeNode* root){}

判断一棵树是否为对称二叉树,可以转换成这棵二叉树的左右子树是否对称,而判断两棵树是否对称,是不是类似判断两棵树是否相等?

判断两棵树相等:p的左 == q的左 && p的右 == q的右

判断两棵树对称:p的左 == q的右 && p的右 == q的左

bool _isSymmetric(struct TreeNode* p, struct TreeNode* q){} 

递归终止:

p == NULL && q == NULL:两个结点都为空,返回true

p == NULL || q == NULL:一个结点为空,一个结点不为空,不对称返回false

p != NULL && q != NULL, p->val != q->val:两个结点都不为空,并且结点的值不同,返回false

递归调用:

p != NULL && q != NULL, p->val == q->val:这两个结点都不为空,并且结点的值相同,递归判断他们左右子树是否对称

左右子树的对称问题可以分解成子问题

//两棵树对称:p的左 == q的右 && p的右 == q的左
bool _isSymmetric(struct TreeNode* p, struct TreeNode* q)
{if(p == NULL && q == NULL)return true;if(p == NULL || q == NULL)return false;if(p->val != q->val)return false;return _isSymmetric(p->left, q->right) && _isSymmetric(p->right, q->left);
}//一棵树是否对称,转换为这棵树的左右子树是否对称
bool isSymmetric(struct TreeNode* root)
{return _isSymmetric(root->left, root->right);
}

4.4 另一棵树的子树

bool isSubtree(struct TreeNode* root, struct TreeNode* subRoot){}

判断树q是否为树p的子树,从递归的思想来看,可以先判断p树和q树是否相等,如果不相等,递归判断p的左右子树是否和q相等即可。

递归终止:

p == NULL:p树递归到空,q不是p的子树,返回false

p != NULL && p == q:q是p的子树,返回true

递归调用:

p != NULL && p != q:不相等,递归判断p的左右子树是否和q相等

判断p的左右子树是否包含 q,可以分解成子问题

bool isSameTree(struct TreeNode* p, struct TreeNode* q)
{if(p == NULL && q == NULL)return true;if(p == NULL || q == NULL)return false;if(p->val != q->val)return false;return isSameTree(p->left, q->left) && isSameTree(p->right, q->right);
}//判断p树是否为q树的子树,先判断p树和q树是否相等,不相等的话再递归q的左右子树和p是否相等
bool isSubtree(struct TreeNode* root, struct TreeNode* subRoot)
{//q为空,不相等if(root == NULL)return false;//判断p树和q树是否相等if(isSameTree(root, subRoot))return true;//p树和q树不相等,递归q的左右子树和p是否相等return isSubtree(root->left, subRoot) || isSubtree(root->right, subRoot);
}

4.5 二叉树翻转

翻转一个二叉树即交换这棵二叉树的左右子树。

递归终止:

root == NULL : 为空,没有左右子树,不用翻转。

递归调用:

root != NULL : 不为空,交换左右子树,再递归翻转左右子树。

左右子树的翻转问题分解成子问题。

void _invertTree(struct TreeNode* root)
{if(root == NULL)return;struct TreeNode* temp = root->left;root->left = root->right;root->right = temp;_invertTree(root->left);_invertTree(root->right); 
}struct TreeNode* invertTree(struct TreeNode* root)
{_invertTree(root);return root;
}

4.6 二叉树的前序遍历

这题要求把遍历的结点val值以数组的形式返回,不同于简单的遍历,但是逻辑和普通的遍历没有太大的区别,主要考察递归调用中局部变量的变化情况。

我们要把结点的val值读入数组中,这里就需要一个变量i来标识数组的下标,但是这里的i就有陷阱了,这也是递归问题中一个常见的问题。

这里的i必须传地址,因为对于递归问题来说,每一层函数栈帧的i都是独立的局部变量,每一层的i都不相等,递归回退上一层后,i值不是原来的i了。

int TreeSize(struct TreeNode* root)
{return root == NULL ? 0 : TreeSize(root->left) + TreeSize(root->right) + 1;
}//这里的i必须传地址,因为对于递归问题来说,每一层函数栈帧的i都是独立的局部变量,并不相等,递归回退上一层后,i值不是原来的i了
void _preorderTraversal(struct TreeNode* root, int* arr, int* pi)
{//root为空if(!root)return;//root不为空,val值放入数组arr[(*pi)++] = root->val;_preorderTraversal(root->left, arr, pi);_preorderTraversal(root->right, arr, pi);
}int* preorderTraversal(struct TreeNode* root, int* returnSize)
{int size = TreeSize(root);int* retArr = (int*)malloc(sizeof(int) * size);*returnSize = size;int i = 0;//对于这种返回值不好处理的递归问题,我们可以设置子函数来解决_preorderTraversal(root, retArr, &i);return retArr;
}

ps:这里也有个设计递归代码的常用小技巧,就是遇到函数返回值不好处理的情况下(比如这里要返回的int*,递归时并不好接收),我们可以写一个子函数并处理这里的返回值即可。

4.7 二叉树的中序遍历

int TreeSize(struct TreeNode* root)
{return root == NULL ? 0 : TreeSize(root->left) + TreeSize(root->right) + 1;
}void _inorderTraversal(struct TreeNode* root, int* arr, int* pi)
{if(!root)return;_inorderTraversal(root->left, arr, pi);arr[(*pi)++] = root->val;_inorderTraversal(root->right, arr, pi);
}int* inorderTraversal(struct TreeNode* root, int* returnSize)
{int size = TreeSize(root);int* retArr = (int*)malloc(sizeof(int) * size);*returnSize = size;int i = 0;_inorderTraversal(root, retArr, &i);return retArr;
}

4.8 二叉树的后序遍历

int TreeSize(struct TreeNode* root)
{return root == NULL ? 0 : TreeSize(root->left) + TreeSize(root->right) + 1;
}void _postorderTraversal(struct TreeNode* root, int* arr, int* pi)
{if(!root)return;_postorderTraversal(root->left, arr, pi);_postorderTraversal(root->right, arr, pi);arr[(*pi)++] = root->val;
}int* postorderTraversal(struct TreeNode* root, int* returnSize)
{int size = TreeSize(root);int* retArr = (int*)malloc(sizeof(int) * size);*returnSize = size;int i = 0;_postorderTraversal(root, retArr, &i);return retArr;
}

4.9 二叉树的创建

一串先序遍历字符串,根据此字符串建立一个二叉树。这题和上一题的前序遍历很相似,一个根据已有二叉树返回前序遍历的数组,一个根据前序遍历字符串来创建二叉树,所以这两道题有一个共同点,就是对数组下标的标识变量i的理解。

BTNode* BTreeCreate(char* str, int* pi)
{if(str[*pi] == '#'){(*pi)++;return NULL;}BTNode* node = (BTNode*)malloc(sizeof(BTNode));node->data = str[(*pi)++];//++优先级  >  *优先级node->left = BTreeCreate(str, pi);node->right = BTreeCreate(str, pi);return node;
}void Inorder(BTNode* root)
{if(!root)return;Inorder(root->left);printf("%c ", root->data);Inorder(root->right);
}int main() 
{char str[100];scanf("%s", str);int i = 0;BTNode* ret = BTreeCreate(str, &i);Inorder(ret);return 0;
}

代码注意:

  1. ++运算符的优先级 > *运算符的优先级, *pi++会先对指针进行++,再解引用,导致代码出错,所以要加括号: ( *pi)++。

  2. 判断#的if语句部分不能( *pi)++,要在if语句的代码块里面++,否则只要判断就会++。就算不是‘#’,i也会被++。

if(str[(*pi)++] == '#')//如果str[*pi]不为'#',i也被++了。return NULL;

相关文章:

二叉树链式存储结构

目录 1.二叉树链式存储结构 2.二叉树的遍历 2.1 前、中、后序遍历 2.2 层序遍历 3.二叉树的其他递归问题 3.1 二叉树的结点个数 ​3.2 二叉树的叶子结点个数 3.3 二叉树第k层结点个数 3.4 二叉树的深度 3.5 二叉树查找 3.6 二叉树销毁 4.二叉树的基础OJ题 4.1 单值…...

Claude 使用指南 | 可与GPT-4媲美的语言模型

本文全程干货,让你轻松使用上claude,这也是目前体验cluade的唯一途径!废话不多说,直接上教程,cluade的能力不逊于GPT4,号称是ChatGPT4.0最强竞品。相对Chatgpt来说,Claude不仅是完全免费的&…...

【汇编】微处理器

【汇编】微处理器 文章目录 【汇编】微处理器1、微处理器概念1.1 关键词1.2 分类 2、微处理器结构2.1 寄存器2.2 寄存器&汇编助记符2.3 寄存器组成结构 3、地址空间3.1 存储空间3.1.1 虚拟空间(编程空间)3.1.2 线性空间 3.2 I/O空间 4、工作模式4.1 …...

按键点亮led灯

原理图: K0这个按键按下时,开发板D1这个灯亮,松开,灯灭 代码如下: #include "stm32f4xx.h" void LED_Init(void) {//1.定义一个GPIO外设的结构体变量 GPIO_InitTypeDef GPIO_InitStructure;//RCC_AHB1PeriphClockCmd(RCC_AHB1Pe…...

Java常见面试题

目录 1、mysql并发事务会带来哪些问题,如何解决?2、请详细描述Redis持久化机制?3、简述Redis缓存雪崩和缓存穿透的问题和解决方案?4、RabbitMQ消息丢失及对应解决方案5、什么叫线程安全?举例说明6、举例说明常用的加密…...

笔记1.5:计算机网络体系结构

从功能上描述计算机网络结构 分层结构 每层遵循某个网络协议完成本层功能 基本概念 实体:表示任何可发送或接收信息的硬件或软件进程。 协议是控制两个对等实体进行通信的规则的集合,协议是水平的。 任一层实体需要使用下层服务,遵循本层…...

【Python】Python 连接字符串应优先使用 join 而不是 +

Python 连接字符串应优先使用 join 而不是 简介 字符串处理在大多数编程程序语言中都不可避免,字符串的连接也是在编程过程中经常需要面对的问题。 Python中的字符串与其他一些程序语言如C、Java有一些不同,它为不 可变对象。 一旦创建便不能改变&…...

uniapp 小程序 父组件调用子组件方法

答案:配合小程序API > this.selectComponent(""),来选择组件,再使用$vm选择组件实例,再调用方法,或者data 1 设置组件的id,如果你的多端,请跟据情况设置ref,class,id,以便通过小…...

Vue-01:MVVM数据双向绑定与Vue的生命周期

一、Vue介绍 1.1 什么是Vue ? Vue是一个渐进式的JavaScript框架,用于构建用户界面。"渐进式"意味着Vue的设计理念是逐步增强应用的功能和复杂性,而不是一次性地引入所有功能。这使得开发者可以根据项目需求选择性地使用Vue的不同特…...

数据通信网络之OSPFv3基础

文章及资源归档至【AIShareLab】,回复 通信系统与网络 可获取。 文章目录 一、目的二、拓扑三、需求四、步骤 一、目的 掌握路由器的IPv6 基础配置。掌握OSPFv3(单区域)的基础配置。 二、拓扑 如图1 所示,三台路由器R1、R2 和R…...

FPGA-结合协议时序实现UART收发器(五):串口顶层模块UART_TOP、例化PLL、UART_FIFO、uart_drive

FPGA-结合协议时序实现UART收发器(五):串口顶层模块UART_TOP、例化PLL、UART_FIFO、uart_drive 串口顶层模块UART_TOP、例化PLL、UART_FIFO、uart_drive,功能实现。 文章目录 FPGA-结合协议时序实现UART收发器(五&…...

我学编程全靠B站了,真香-国外篇(第三期)

你好,我是Martin。 今天来点猛料,给大家推荐点我的压箱收藏-国外知名大学的公开课。 我推荐的不多,本着少就是多的原则,只给大家推荐我看过最好的五门视频,主要是来自两所国外高校:MIT美国麻省理工、CMU卡…...

c++ 变量常量指针练习题

Q1:在win32 x86模式下,int *p; int **pp; double *q; 请说明p、pp、q各占几个字节的内存单元。 p 占 4 个字节 pp 占 4 个字节 q 占 4 个字节 Q2常量1、1.0、“1”的数据类型是什么? 1 是 整形 int 1.0 是 浮点型 double “1” 是 const char * Q3 语句&…...

Linux底层基础知识

一.汇编,C语言,C,JAVA之间的关系 汇编,C语言,C可以通过不同的编译器,编译成机器码。而java只能由Java虚拟机识别。Java虚拟机可以看成一个操作系统,Java虚拟机是由汇编,C&#xff0c…...

JUC并发编程--------线程安全篇

目录 什么是线程安全性问题? 如何实现线程安全? 1、线程封闭 2、无状态的类 3、让类不可变 4、加锁和CAS 并发环境下的线程安全问题有哪些? 1、死锁 2、活锁 3、线程饥饿 什么是线程安全性问题? 我们可以这么理解&#…...

机器视觉之Basler工业相机使用和配置方法(C++)

basler工业相机做双目视觉用,出现很多问题记录一下: 首先是多看手册:https://zh.docs.baslerweb.com/software 手册内有所有的源码和参考示例,实际上在使用过程中,大部分都是这些源码,具体项目选择对应的…...

Centos nginx配置文档

1、安装nginx: yum install nginx 2、Nginx常用命令 查看版本:nginx -v 启动:nginx -c /etc/nginx/nginx.conf 重新加载配置:nginx -s reload 停止:nginx -s stop 3、Nginx反向代理配置 nginx配置详解 1、Nginx配置图 详情可以查看:http://nginx.org/ru/docs/example…...

2023/9/14 -- C++/QT

作业&#xff1a; 仿照Vector实现MyVector&#xff0c;最主要实现二倍扩容 #include <iostream>using namespace std;template <typename T> class MyVector { private:T *data;size_t size;size_t V_capacity; public://无参构造MyVector():data(nullptr),size(…...

golang在goland编译时获取环境变量失效

在golang中&#xff0c; 我们通常使用os包来获取环境变量&#xff0c;如&#xff1a; os.Getenv() os.LookupEnv() 等。 但如果我们使用goland编译器&#xff0c;在编译是&#xff0c;这时操作环境变量&#xff0c;会发现os包读取到的环境变量值不变&#xff1a; 新增后&am…...

一款非常容易上手的报表工具,简单操作实现BI炫酷界面数据展示,驱动支持众多不同类型的数据库,可视化神器,免开源了

一款非常容易上手的报表工具&#xff0c;简单操作实现BI炫酷界面数据展示&#xff0c;驱动支持众多不同类型的数据库&#xff0c;可视化神器&#xff0c;免开源了。 在互联网数据大爆炸的这几年&#xff0c;各类数据处理、数据可视化的需求使得 GitHub 上诞生了一大批高质量的…...

蓝桥杯 题库 简单 每日十题 day3

01 约数个数 题目描述 本题为填空题&#xff0c;只需要算出结果后&#xff0c;在代码中使用输出语句将所填结果输出即可。 1200000 有多少个约数&#xff08;只计算正约数&#xff09;。 解题思路 枚举&#xff0c;从1开始一直到1200000本身都作为1200000的除数&#xff0c;…...

基于SSM+Vue的高校实验室管理系统的设计与实现

末尾获取源码 开发语言&#xff1a;Java Java开发工具&#xff1a;JDK1.8 后端框架&#xff1a;SSM 前端&#xff1a;采用Vue技术开发 数据库&#xff1a;MySQL5.7和Navicat管理工具结合 服务器&#xff1a;Tomcat8.5 开发软件&#xff1a;IDEA / Eclipse 是否Maven项目&#x…...

C语言天花板——指针(初阶)

&#x1f320;&#x1f320;&#x1f320; 大家在刚刚接触C语言的时候就肯定听说过&#xff0c;指针的重要性以及难度等级&#xff0c;以至于经常“谈虎色变”&#xff0c;但是今天我来带大家走进指针的奇妙世界。&#x1f387;&#x1f387;&#x1f387; 一、什么是指针&…...

关于第一届全球电子纸创新应用金奖征集评选及报名指南

重要通知 &#xff5c;关于第一届全球电子纸创新应用金奖征集评选及报名指南https://mp.weixin.qq.com/s/RWsZtmJ20-NZXMG0k0rwPA?wxwork_useridEPIA 从2004年&#xff0c;Sony推出全球首款电纸书阅读器至今20载&#xff0c;这期间&#xff0c;到底诞生了多少种创新产品&#…...

idea搭建项目找不到Tomcat

idea搭建项目找不到Tomcat_idea没有tomcat配置项_ZYRL的博客-CSDN博客...

类和对象三大特性之继承

全文目录 继承的概念定义格式继承关系和访问限定符final 基类和派生类对象赋值转换继承中的作用域派生类的六个默认成员函数构造函数拷贝构造函数operator析构函数 友元和静态成员友元静态成员 各种继承形式菱形继承虚继承菱形虚拟继承对象模型 继承和组合 继承的概念 通过继承…...

Debian 12安装Docker

1.更新系统包 #apt update 2.安装依赖包 #apt install apt-transport-https ca-certificates curl gnupg lsb-release 3.添加Docker源 &#xff08;1&#xff09;添加Docker 官方GPG密钥 #curl -fsSL https://download.docker.com/linux/debian/gpg | gpg --dearmor -o /usr/s…...

小谈设计模式(4)—单一职责原则

小谈设计模式&#xff08;4&#xff09;—单一职责原则 专栏介绍专栏地址专栏介绍 单一职责原则核心思想职责的划分单一变化原则高内聚性低耦合性核心总结 举例图书类&#xff08;Book&#xff09;用户类&#xff08;User&#xff09;图书管理类&#xff08;Library&#xff09…...

ATF(TF-A) EL3 SPMC威胁模型-安全检测与评估

安全之安全(security)博客目录导读 ATF(TF-A) 威胁模型汇总 目录 一、简介 二、评估目标 1、数据流图 三、威胁分析 1、信任边界 2、资产 3、威胁代理 4、威胁类型 5、威胁评估 5.1 端点在直接请求/响应调用中模拟发送方FF-A ID 5.2 端点在直接请求/响应调用中模拟…...

AI绘画Stable Diffusion原理之扩散模型DDPM

前言 传送门&#xff1a; stable diffusion&#xff1a;Git&#xff5c;论文 stable-diffusion-webui&#xff1a;Git Google Colab Notebook部署stable-diffusion-webui&#xff1a;Git kaggle Notebook部署stable-diffusion-webui&#xff1a;Git AI绘画&#xff0c;输入一段…...

重装wordpress图片不见/全网热度指数

1、 管道概述及相关API应用 1.1 管道相关的关键概念 管道是Linux 支持的最初Unix IPC形式之一&#xff0c;具有以下特点&#xff1a; 管道是半双工的&#xff0c;数据只能向一个方向流动&#xff1b;需要双方通信时&#xff0c;需要建立起两个管道&#xff1b;只能用于父子进…...

网站建设需要到哪些知识/网络运营培训课程

Web后端开发者应该对依赖注入都比较熟悉&#xff0c;至于Android又是如何进行依赖注入的呢&#xff1f;在这篇文章中&#xff0c;让我们一起通过一个例子了解一下在Android中进行依赖注入的好处。 AndroidAnnotations AndroidAnnotations是一个致力于加快应用开发速度的Android…...

韩国大型门户网站/百度一下百度搜索官网

在读这篇文章之前&#xff0c;请确定你已经了解MIP定义及加速原理。如果不确定的话&#xff0c;可以到MIP官网了解。 改造前期准备和注意事项: 你可以选择直接将原先的移动站点直接改成MIP站&#xff0c;也可以单独再做一套MIP站点与移动站并存。 复杂的页面暂不建议MIP改造&am…...

影响网站收录的因素/中国没有限制的搜索引擎

问题截图&#xff1a; 问题可能原因&#xff1a; 1、这一点很重要&#xff1a;就是在pom.xml没有插件或者没有开启在java目录下的xml、properties文件扫描 <build><!--目的是把src/main/java目录中的xml文件包含到输出结果中。输出到classes目录中--><resour…...

深圳网站制作哪里好/南昌seo服务

在学习方面&#xff0c;两者没有好坏之分。只要我们认真学习一种知识&#xff0c;努力把相关的知识学好&#xff0c;那么两者都是很好的选择。如果你想知道学习Python和Java哪个更好&#xff0c;这取决于你从事的是哪种工作。如果是大型企业项目&#xff0c;最好选择Java进行一…...

新网站怎么发外链/电脑培训机构

将我正在制作的游戏应用程序导出为可运行的.jar文件后&#xff0c;我从命令提示符处运行了它&#xff0c;只是发现了一些问题。 我不得不解决一些问题&#xff0c;找不到音频路径&#xff0c;也无法读取/写入.txt文件。 我看过并能够解决的音频文件&#xff0c;我看过并接近解决…...