解锁二叉树的魅力:链式实现详解

server/2024/10/18 10:46:51/

前言

二叉树的简介及顺序实现

引言

在数据结构的浩瀚星空中,二叉树如同一颗璀璨的明珠,其优雅的结构和强大的功能使其成为计算机科学中不可或缺的工具。从数据库索引到编译器的语法树,二叉树以其独特的方式支撑着许多核心算法与数据处理。本文将深入探讨如何使用C语言实现二叉树的链式结构,并详细讲解各个部分的实现。

一、二叉树的结构

1.1结构

二叉树(binary tree) 是一种非线性数据结构,代表“祖先”与“后代”之间的派生关系,体现了“一分为二” 的分治逻辑。与链表类似,二叉树的基本单元是节点,每个节点包含值、左子节点引用和右子节点引用。
//二叉树节点结构
typedef int DataType;
typedef struct BinaryTreeNode
{int data;//值域struct BinaryTreeNode* left;//指向当前节点左孩子struct BinaryTreeNode* right;//指向当前节点右孩子
}BTNode;
每个节点都有两个引用(指针),分别指向 左子节点(left‑child node) 右子节点(right‑child node) ,该节点被称为这两个子节点的 父节点(parent node) 。当给定一个二叉树的节点时,我们将该节点的左子节点及其以下节点形成的树称为该节点的 左子树(left subtree) ,同理可得 右子树(right subtree)

1.2链式实现的优势

选择链式实现二叉树有几个显著的优点:

  1. 动态大小:链式结构能够根据需要动态分配内存,避免了固定大小的限制,适合处理不确定数量的数据。
  2. 节省空间:链式结构只为实际存在的节点分配内存,减少了空间浪费,相比数组实现更加高效。
  3. 简化操作:插入和删除操作无需移动其他元素,操作更为高效,使得二叉树在频繁变动的数据环境中表现出色。

二、二叉树的基本操作

⼆叉树的创建⽅式⽐较复杂,为了更好的步⼊到⼆叉树内容中,我们先⼿动创建⼀棵链式⼆叉树:
BTNode* buyNode(DataType x)
{BTNode* newnode = (BTNode*)malloc(sizeof(BTNode));if (newnode == NULL){perror("malloc fail!");exit(1);}newnode->data = x;newnode->left=newnode->right = NULL;}
BTNode* CreateTree()
{BTNode* n1 = buyNode(1);BTNode* n2 = buyNode(2);BTNode* n3 = buyNode(3);BTNode* n4 = buyNode(4);BTNode* n5 = buyNode(5);BTNode* n6 = buyNode(6);n1->left = n2;n1->right = n4;n2->left = n3;n4->left = n5;n4->right = n6;return n1;
}

2.1前序遍历

前序遍历是一种深度优先遍历方式,按照“根节点 -> 左子树 -> 右子树”的顺序访问树中的节点。它是树结构中非常重要的一种遍历方式,常用于复制树结构和表达式树的解析等场景。

访问顺序

  1. 首先访问当前节点(根节点)。
  2. 然后递归访问左子树。
  3. 最后递归访问右子树。

应用场景

  • 生成树的序列化表示。
  • 复制树结构。
  • 在表达式树中用于构建前缀表达式。

递归实现:

//前序遍历
//访问优先级:根节点 -> 左子树 -> 右子树
void PreOrder(BTNode* root)
{if (root == NULL){return;}printf("%d ", root->data);PreOrder(root->left);PreOrder(root->right);
}

在这个实现中,我们首先检查当前节点是否为空。如果不为空,我们打印当前节点的数据,然后递归地访问左子节点和右子节点。这种顺序确保了根节点总是最早被访问,对于许多应用场景非常有效。

2.2中序遍历

中序遍历是一种深度优先遍历方式,按照“左子树 -> 根节点 -> 右子树”的顺序访问树中的节点。它在树结构中扮演着重要的角色,常用于生成排序的节点序列以及表达式树的解析。

访问顺序

  1. 首先递归访问左子树。
  2. 然后访问当前节点(根节点)。
  3. 最后递归访问右子树。

应用场景

  • 生成有序的节点序列。
  • 在表达式树中用于构建中缀表达式。

递归实现:

//中序遍历
//访问优先级:左子树 -> 根节点 -> 右子树
void InOrder(BTNode* root)
{if (root == NULL){return;}InOrder(root->left);printf("%d ", root->data);InOrder(root->right);
}

在这个实现中,我们首先检查当前节点是否为空。如果不为空,我们递归地访问左子节点,接着打印当前节点的数据,最后递归地访问右子节点。这种顺序确保了我们在访问节点时能够得到有序的结果。

2.3后序遍历

后序遍历是一种深度优先遍历方式,按照“左子树 -> 右子树 -> 根节点”的顺序访问树中的节点。这种遍历方式在某些情况下非常有用,尤其是在需要先处理子节点再处理父节点的场景中。

访问顺序

  1. 首先递归访问左子树。
  2. 然后递归访问右子树。
  3. 最后访问当前节点(根节点)。

应用场景

  • 计算树的高度或大小。
  • 删除树结构时,确保先删除子节点。
  • 在表达式树中用于构建后缀表达式。

递归实现:

//后序遍历
//访问优先级:左子树 -> 右子树 -> 根节点
void PostOrder(BTNode* root)
{if (root == NULL){return;}PostOrder(root->left);PostOrder(root->right);printf("%d ", root->data);
}

在这个实现中,我们首先检查当前节点是否为空。如果不为空,我们递归地访问左子节点,然后递归地访问右子节点,最后打印当前节点的数据。这种顺序确保了我们在访问节点时能够正确处理所有子节点之后再处理父节点,适用于需要反向处理的场景。

2.4层序遍历

层序遍历是一种广度优先遍历方式,按照“从上到下、从左到右”的顺序访问树中的节点。它在需要逐层处理节点的场景中非常有用,例如按层打印树或计算树的深度。

步骤如下:
1.初始化一个队列,将根节点入队。
2.当队列不为空,重复以下操作:

  • 从队列中出队一个节点并访问(打印或记录)。
  • 将该节点的左子节点和右子节点(如果存在)入队。

 代码实现:

//层序遍历 借助数据结构--队列
void LevelOrder(BTNode* root)
{QU q;//创建一个队列QueueInit(&q);QueuePush(&q, root);//根节点入队,成为队头while (!QueueEmpty(&q))//队列不为空{//取队头,打印BTNode*Qhead=QueueFront(&q);printf("%d ", Qhead->data);QueuePop(&q);//队头出队//队头节点左孩子入队if (Qhead->left){QueuePush(&q, Qhead->left);}//队头节点右孩子入队if (Qhead->right){QueuePush(&q, Qhead->right);}}QueueInit(&q);QueueDestroy(&q);
}

2.5求二叉树节点个数

步骤如下:

1.如果当前节点为空,返回0(没有节点)。

2.如果当前节点不为空,返回1(当前节点)加上左子树和右子树的节点个数。

 代码实现:

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

2.6求二叉树叶子节点数

  • 叶子节点:没有子节点的节点。
  • 非叶子节点:至少有一个子节点的节点。

步骤如下:

1.如果当前节点为空,返回0(没有叶子节点)。

2.如果当前节点是叶子节点(左右子节点均为空),返回1。

3.如果当前节点不是叶子节点,递归计算左子树和右子树的叶子节点个数,并将结果相加。

 代码实现:

//⼆叉树叶子结点个数 叶子节点:度为0的节点
int BTLeafSize(BTNode* root)
{if (root == NULL){return 0;}if (root->left == NULL && root->right == NULL){return 1;}return BTLeafSize(root->left) + BTLeafSize(root->right);
}

2.7求⼆叉树第k层结点个数 

步骤如下:

1.如果当前节点为空,返回0。

2.如果当前层数等于k,返回1。

3.如果当前层数小于k,递归计算左子树和右子树第k层的节点个数。

代码实现:

//⼆叉树第k层结点个数 
int BTLevelKSize(BTNode* root, int k)
{if (root == NULL){return 0;}if (k == 1){return 1;}return BTLevelKSize(root->left, k - 1) + BTLevelKSize(root->right, k - 1);
}

2.8求⼆叉树的高度

步骤如下:

1.如果当前节点为空,返回0(树的高度为0)。

2.递归计算左子树和右子树的高度。

3.返回左子树和右子树高度中的较大值,加1(表示当前节点的高度)。

代码实现:

//⼆叉树的深度/高度
int BTDepth(BTNode* root)
{if (root == NULL){return 0;}//返回左右子树最深的那个return BTDepth(root->left) > BTDepth(root->right) ? BTDepth(root->left) + 1 : BTDepth(root->right) + 1;
}

2.9二叉树查值

步骤如下:

1.如果当前节点为空,返回 NULL(未找到)。

2.如果当前节点的值等于 x,返回当前节点。

3.如果当前节点的值不等于 x,递归查找左子树和右子树。

4.如果在左子树中找到,返回该节点;否则,继续在右子树中查找。

代码实现:

//⼆叉树查找值为x的结点 
BTNode* BTFind(BTNode* root, DataType x)
{if (root == NULL){return NULL;}if (root->data == x){return root;}BTNode* leftFind=BTFind(root->left, x);//左子树中找if (leftFind){return leftFind;//左子树找到了,返回}BTNode* rightFind=BTFind(root->right, x);//右子树中找if (rightFind){return rightFind;//右子树找到了,返回}return NULL;
}

2.9判断完全二叉树

方法概述

  1. 队列初始化:使用队列来进行层序遍历。
  2. 遍历过程
    • 将根节点入队,开始遍历。
    • 从队列中出队节点,检查其是否为空。
    • 对于每个非空节点,将其左右孩子入队。
    • 一旦遇到空节点,标记后续节点必须都是空节点。
  3. 最终验证:继续遍历队列,确保所有后续节点均为空。

代码如下:

//判断二叉树是否为完全二叉树--借助队列
bool BTComplete(BTNode* root)
{QU q;//创建一个队列QueueInit(&q);QueuePush(&q, root);//根节点入队,成为队头while (!QueueEmpty(&q))//队列不为空{//取队头出队BTNode* Qhead = QueueFront(&q);QueuePop(&q);if (Qhead == NULL){break;}//左右孩子都入队QueuePush(&q, Qhead->left);QueuePush(&q, Qhead->right);}//空节点入对头是跳出循环,此时队列不一定为空while (!QueueEmpty(&q))//队列不为空{//取队头,出队BTNode* Qhead = QueueFront(&q);QueuePop(&q);if (Qhead != NULL){return false;//此时取到非空节点说明不是完全二叉树}}QueueDestroy(&q);return true;//是完全二叉树
}

2.10二叉树销毁

代码实现:

//⼆叉树销毁 
//后序遍历销毁:先销毁左子树,再右子树,最后根节点
void BTDestroy(BTNode** root)
{if (*root == NULL){return;}BTDestory(&((*root)->left));BTDestory(&((*root)->right));free(*root);*root = NULL;
}
//层序遍历 借助数据结构--队列
void LevelOrder(BTNode* root)
{QU q;//创建一个队列QueueInit(&q);QueuePush(&q, root);//根节点入队,成为队头while (!QueueEmpty(&q))//队列不为空{//取队头,打印BTNode*Qhead=QueueFront(&q);printf("%d ", Qhead->data);QueuePop(&q);//队头出队//队头节点左孩子入队if (Qhead->left){QueuePush(&q, Qhead->left);}//队头节点右孩子入队if (Qhead->right){QueuePush(&q, Qhead->right);}}QueueInit(&q);QueueDestroy(&q);
}

三、完整代码

Queue.h

队列的声明

#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<stdbool.h>
//typedef int DataType;
typedef struct BinaryTreeNode* QDataType;
//定义节点结构体
typedef struct Node
{QDataType data;//数据域struct Node* next;//指针域
}Node;
//定义队列结构体
typedef struct Queue
{Node* phead;//队头Node* ptail;//队尾int size;//队列长度
}QU;
//初始化队列 
void QueueInit(QU* p);
//入队列,队尾
void QueuePush(QU* p, QDataType x);
//队列判空 
bool QueueEmpty(QU* p);
//出队列,队头 
void QueuePop(QU* p);
//取队头数据 
QDataType QueueFront(QU* p);
//取队尾数据 
QDataType QueueBack(QU* p);
//队列长度
int QueueSize(QU* p);
//销毁队列 
void QueueDestroy(QU* p);

Queue.c 

队列的定义

#define _CRT_SECURE_NO_WARNINGS
#include"Queue.h"
// 初始化队列
void QueueInit(QU* p)
{assert(p);p->phead = p->ptail = NULL; // 头尾指针初始化为 NULLp->size = 0; // 队列大小初始化为 0
}// 创建新节点
Node* CreateNode(QDataType x)
{Node* newnode = (Node*)malloc(sizeof(Node));if (newnode == NULL) {perror("malloc fail");exit(1); // 分配失败,退出程序}newnode->data = x; // 设置节点数据newnode->next = NULL; // 初始化后继指针为 NULLreturn newnode; // 返回新节点
}// 入队操作(从队尾插入)
void QueuePush(QU* p, QDataType x)
{assert(p);Node* newnode = CreateNode(x);if (p->phead == NULL) // 队列为空时{p->phead = p->ptail = newnode; // 头尾指针都指向新节点}else // 队列不为空时{p->ptail->next = newnode; // 尾节点的 next 指向新节点p->ptail = newnode; // 更新尾指针为新节点}++p->size; // 队列大小加 1
}// 判断队列是否为空
bool QueueEmpty(QU* p)
{assert(p);return p->phead == NULL; // 头指针为 NULL 表示队列为空
}// 出队操作(从队头删除)
void QueuePop(QU* p)
{assert(p);assert(!QueueEmpty(p)); // 确保队列不为空if (p->phead == p->ptail) // 只有一个元素时{free(p->phead); // 释放头节点p->phead = p->ptail = NULL; // 更新头尾指针为 NULL}else{Node* del = p->phead; // 保存要删除的节点p->phead = p->phead->next; // 更新头指针为下一个节点free(del); // 释放删除的节点}--p->size; // 队列大小减 1
}// 获取队头数据
QDataType QueueFront(QU* p)
{assert(p);assert(!QueueEmpty(p)); // 确保队列不为空return p->phead->data; // 返回头节点的数据
}// 获取队尾数据
QDataType QueueBack(QU* p)
{assert(p);assert(!QueueEmpty(p)); // 确保队列不为空return p->ptail->data; // 返回尾节点的数据
}// 获取队列长度
int QueueSize(QU* p)
{assert(p);return p->size; // 返回队列的大小
}// 销毁队列
void QueueDestroy(QU* p)
{assert(p);Node* pcur = p->phead;while (pcur) // 遍历所有节点并释放内存{Node* next = pcur->next;free(pcur);pcur = next;}p->phead = p->ptail = NULL; // 头尾指针置为 NULLp->size = 0; // 队列大小置为 0
}

Tree.h

二叉树的声明

#pragma once
#include<stdio.h>
#include<assert.h>
#include<stdlib.h>
#include<stdbool.h>
//定义二叉树链式结构
//二叉树节点结构
typedef int DataType;
typedef struct BinaryTreeNode
{int data;//值域struct BinaryTreeNode* left;//指向当前节点左孩子struct BinaryTreeNode* right;//指向当前节点右孩子
}BTNode;
//前序遍历--根左右
void PreOrder(BTNode* root);
//中序遍历--左根右
void InOrder(BTNode* root);
//二叉树结点个数 
int BTSize(BTNode* root); 
//二叉树叶子结点个数 
int BTLeafSize(BTNode* root);
//二叉树第k层结点个数 
int BTLevelKSize(BTNode* root, int k); 
//二叉树的深度/高度
int BTDepth(BTNode* root); 
//二叉树查找值为x的结点 
BTNode* BTFind(BTNode* root, DataType x); 
//二叉树销毁 
void BTDestroy(BTNode** root);
//层序遍历--借助队列
void LevelOrder(BTNode* root);
//判断二叉树是否为完全二叉树--借助队列
bool BTComplete(BTNode* root);

Tree.c

二叉树的实现

#define _CRT_SECURE_NO_WARNINGS
#include"Tree.h"
#include"Queue.h"
//前序遍历
//访问优先级:根节点 -> 左子树 -> 右子树
void PreOrder(BTNode* root)
{if (root == NULL){return;}printf("%d ", root->data);PreOrder(root->left);PreOrder(root->right);
}
//中序遍历
//访问优先级:左子树 -> 根节点 -> 右子树
void InOrder(BTNode* root)
{if (root == NULL){return;}InOrder(root->left);printf("%d ", root->data);InOrder(root->right);
}
//后序遍历
//访问优先级:左子树 -> 右子树 -> 根节点
void PostOrder(BTNode* root)
{if (root == NULL){return;}PostOrder(root->left);PostOrder(root->right);printf("%d ", root->data);
}
//⼆叉树结点个数 
int BTSize(BTNode* root)
{if (root == NULL){return 0;}return 	BTSize(root->left) + BTSize(root->right) + 1;
}
//⼆叉树叶子结点个数 叶子节点:度为0的节点
int BTLeafSize(BTNode* root)
{if (root == NULL){return 0;}if (root->left == NULL && root->right == NULL){return 1;}return BTLeafSize(root->left) + BTLeafSize(root->right);
}
//⼆叉树第k层结点个数 
int BTLevelKSize(BTNode* root, int k)
{if (root == NULL){return 0;}if (k == 1){return 1;}return BTLevelKSize(root->left, k - 1) + BTLevelKSize(root->right, k - 1);
}
//⼆叉树的深度/高度
int BTDepth(BTNode* root)
{if (root == NULL){return 0;}//返回左右子树最深的那个return BTDepth(root->left) > BTDepth(root->right) ? BTDepth(root->left) + 1 : BTDepth(root->right) + 1;
}
//⼆叉树查找值为x的结点 
BTNode* BTFind(BTNode* root, DataType x)
{if (root == NULL){return NULL;}if (root->data == x){return root;}BTNode* leftFind=BTFind(root->left, x);//左子树中找if (leftFind){return leftFind;//左子树找到了,返回}BTNode* rightFind=BTFind(root->right, x);//右子树中找if (rightFind){return rightFind;//右子树找到了,返回}return NULL;
}
//⼆叉树销毁 
//后序遍历销毁:先销毁左子树,再右子树,最后根节点
void BTDestroy(BTNode** root)
{if (*root == NULL){return;}BTDestroy(&((*root)->left));BTDestroy(&((*root)->right));free(*root);*root = NULL;
}
//层序遍历 借助数据结构--队列
void LevelOrder(BTNode* root)
{QU q;//创建一个队列QueueInit(&q);QueuePush(&q, root);//根节点入队,成为队头while (!QueueEmpty(&q))//队列不为空{//取队头,打印BTNode*Qhead=QueueFront(&q);printf("%d ", Qhead->data);QueuePop(&q);//队头出队//队头节点左孩子入队if (Qhead->left){QueuePush(&q, Qhead->left);}//队头节点右孩子入队if (Qhead->right){QueuePush(&q, Qhead->right);}}QueueInit(&q);QueueDestroy(&q);
}
//判断二叉树是否为完全二叉树--借助队列
bool BTComplete(BTNode* root)
{QU q;//创建一个队列QueueInit(&q);QueuePush(&q, root);//根节点入队,成为队头while (!QueueEmpty(&q))//队列不为空{//取队头出队BTNode* Qhead = QueueFront(&q);QueuePop(&q);if (Qhead == NULL){break;}//左右孩子都入队QueuePush(&q, Qhead->left);QueuePush(&q, Qhead->right);}//空节点入对头是跳出循环,此时队列不一定为空while (!QueueEmpty(&q))//队列不为空{//取队头,出队BTNode* Qhead = QueueFront(&q);QueuePop(&q);if (Qhead != NULL){return false;//此时取到非空节点说明不是完全二叉树}}QueueDestroy(&q);return true;//是完全二叉树
}

main.c

测试

#define _CRT_SECURE_NO_WARNINGS
#include"Tree.h"
BTNode* buyNode(DataType x)
{BTNode* newnode = (BTNode*)malloc(sizeof(BTNode));if (newnode == NULL){perror("malloc fail!");exit(1);}newnode->data = x;newnode->left=newnode->right = NULL;}
BTNode* CreateTree()
{BTNode* n1 = buyNode(1);BTNode* n2 = buyNode(2);BTNode* n3 = buyNode(3);BTNode* n4 = buyNode(4);BTNode* n5 = buyNode(5);BTNode* n6 = buyNode(6);n1->left = n2;n1->right = n4;n2->left = n3;n4->left = n5;n4->right = n6;return n1;
}
void Test()
{BTNode* n1 = CreateTree();printf("\n前序遍历:");PreOrder(n1);printf("\n中序遍历:");InOrder(n1);printf("\n后续遍历:");PostOrder(n1);//打印节点数printf("\nsize=%d\n", BTSize(n1));//打印叶子节点个数printf("left_size=%d\n", BTLeafSize(n1));int k = 2;printf("第%d层节点个数:%d\n", k,BTLevelKSize(n1,k));printf("Depth:%d\n", BTDepth(n1));//查找数字4BTNode* find = BTFind(n1, 4);printf("%s\n", find == NULL ? "未找到" : "找到了!");printf("\n层序遍历:");LevelOrder(n1);bool ret= BTComplete(n1);printf("\n%s\n",ret==false ?"不是完全二叉树" : "是完全二叉树!");BTDestroy(&n1);
}
int main()
{Test();return 0;
}

结果如下:

四、总结

在这篇文章中,我们深入探讨了二叉树的链式结构及其在C语言中的实现。我们首先了解了二叉树的基本结构及其动态大小的优势,然后介绍了多种遍历方式,包括前序、中序、后序和层序遍历,以及相关的节点统计函数。接着,文章详细阐述了如何判断二叉树是否为完全二叉树,并提供了二叉树的销毁函数。最后,通过实例测试了这些操作,展示了二叉树在数据处理中的重要性和灵活性,为读者提供了全面的理解和实用的代码实现。 


http://www.ppmy.cn/server/132743.html

相关文章

sqoop搭建教程

1.上传并解压 tar -zxvf sqoop-1.4.6.bin__hadoop-2.0.4-alpha.tar.gz2.修改配置文件 cd sqoop-1.4.6/conf/mv sqoop-env-template.sh sqoop-env.shvim sqoop-env.sh3.配置环境变量 vim /etc/profilesource /etc/profile4.添加jar包 cd /usr/local/soft/sqoop-1.4.6/lib

安装vue发生异常:npm ERR! the command again as root/Administrator.

一、异常 npm ERR! The operation was rejected by your operating system. npm ERR! Its possible that the file was already in use (by a text editor or antivirus), npm ERR! or that you lack permissions to access it. npm ERR! npm ERR! If you believe this might b…

C++设计模式 单例模式

单例模式是一种常用的软件设计模式&#xff0c;它保证一个类只有一个实例&#xff0c;并提供一个全局访问点。下面是一个使用 C11 特性编写的线程安全的单例模式示例&#xff1a; #include <iostream> #include <mutex> // For thread safety #include <memory…

【Nuvoton干货分享】开发应用篇 5 -- 32bit MCU Flash 操作

在实际开发中&#xff0c;我们都会碰到需要把部分数据存放在不易失存储空间上&#xff0c;比如外部NOR FLASH、EEPROM、SD等存储空间上&#xff0c;针对数据量不大的情况下&#xff0c;可以考虑将数据存放在芯片ROM存储空间。Nuvoton 32bit MCU ROM存储空间包括LDROM、APROM、S…

深入理解WPF中的命令机制

Windows Presentation Foundation&#xff08;WPF&#xff09;是微软推出的一种用于构建桌面客户端应用程序的技术。它被认为是现代Windows应用程序的基础&#xff0c;具有强大的图形和媒体处理能力。在WPF中&#xff0c;“命令”是一个重要的概念&#xff0c;它为应用程序开发…

关于FFmpeg【使用方法、常见问题、解决方案等】

1、提取音频 【1】提取无损高质量音频文件 问题描述 想要从视频文件中提取出无损高质量音频文件。 解决方案 ffmpeg -i input.mp4 -vn -c:a pcm_s16le -ar 44100 -ac 2 output.wav

5G 技术对智能交通系统有哪些潜在的影响?

5G技术对智能交通系统&#xff08;ITS&#xff09;的潜在影响是多方面的&#xff0c;包括但不限于以下几个关键领域&#xff1a; 车联网与自动驾驶&#xff1a;5G技术通过其高速率、低时延和高可靠性&#xff0c;极大地促进了车联网&#xff08;V2X&#xff09;技术的发展。这包…

网络爬虫-Python网络爬虫和C#网络爬虫

爬虫是一种从互联网抓取数据信息的自动化程序&#xff0c;通过 HTTP 协议向网站发送请求&#xff0c;获取网页内容&#xff0c;并通过分析网页内容来抓取和存储网页数据。爬虫可以在抓取过程中进行各种异常处理、错误重试等操作&#xff0c;确保爬取持续高效地运行 1、Python网…