分类 默认分类 下的文章

计网实验二:制作双绞线

没啥实际的东西,多百度、多复制的就行。

实验目的

(1)了解双绞线布线标准。

(2)掌握直通式双绞线的制作方法。

(3)掌握交叉式双绞线的制作方法。

(4)掌握测线器的使用方法。

实验原理

双绞线的布线标准

20世纪90年代,综合布线引入国内初期,主要采用国外产品因而设计标准也是采用国外标准.原邮电部和信息产业部从1997年开始先后编制、批准、发布一些标准和规范,主要有:《大楼通信综合布线系统第一部分:总规范》YD/T926.1-2001 ,《大楼通信综合布线系统第一部分:综合布线用电缆、光缆技术要求》YD/T926.2-2001 ,《大楼通信综合布线系统第一部分:综合布线用连接硬件技术要求》YD/T926.3-2001 ,《智能建筑设计标准》GB/T50314,《综合布线系统工程设计规范》(GB50311-2007),《综合布线工程验收规范》(GB50312-2007),以此规范网络综合布线系统设计、施工、测试、维护等诸多环节.1.2 网络综合布线系统工程设计原则

网络综合布线系统的设计应遵循如下设计原则:
1 )用户至上;2)可靠性与稳定性;3)技术先进性;4)兼容性;5)灵活性;6)实用性与经济性;7)扩展性

智能化建筑的建设,综合布线系统是基础,它为信息资源的传递提供了物理通道,是现今和未来计算机网络和信息系统的基础支撑环境.它的质量优劣对于工程建设项目和今后的使用都有极为重大的影响,特别是综合布线系统产品的选型最为关键,是工程建设中的热门话题,对产品的选型应遵循以下原则:

1 )必须结合工程的实际情况,以满足用户客观要求;

2)要按照系统工程的特点,局部必须服从整体;

3)产品应符合我国国情和有关标准的规定;

4)应符合技术先进和经济合理相互统一的原则;

5)产品选型应注意近期和远期规划的结合.

线序

ANSI/TIA-568

PinT568A pairT568A colorT568B pairT568B color10BASE-T/100BASE-TX signal1000BASE-T/10GBASE-T signalWire
13Pair 3 Tip 绿白2Pair 2 Tip橙白TD+DA+tip
23Pair 3 Ring绿2Pair 2 RingTD−DA−ring
32Pair 2 Tip橙白3Pair 3 Tip 绿白RD+DB+tip
41Pair 1 Ring1Pair 1 RingNCDC+ring
51Pair 1 Tip蓝白1Pair 1 Tip蓝白NCDC−tip
62Pair 2 Ring3Pair 3 Ring绿RD−DB−ring
74Pair 4 Tip棕白4Pair 4 Tip棕白NCDD+tip
84Pair 4 Ring4Pair 4 RingNCDD−ring

直通线

用于交换设备与终端(电脑)的连接,线头的线序使用同一线序。更加准确的:如果设备使用不同的引脚发送数据,则使用直连线。

交叉线

用于同型设备(交换设备之间或终端电脑之间)的连接,线头的线序使用两个线序(一头使用A线序,另一头则使用B线序)。更加准确的:如果设备使用相同的引脚发送数据,则使用交叉线。

测线仪

  • 装电池
  • 测试仪可以测试两种线,一种是RJ45接头的网线,另一种是RJ11接头的电话线。同时测试仪可以拆分为两边。
  • 把需要测试的网线的两端分别插入测试仪的左边的网线接口和右边的网线接口,打开开关,调慢速或快速都可以
  • 如果是直通线,在网线正常的情况下两边灯亮的顺序都应该为1-8.如果是交叉线则一边的灯亮顺序为1-8,另一边的顺序为36145278.如果灯不是按这个顺序亮,则代表线序做错,如果有某个灯不亮,则代表这个灯所处的线不能正常导通,也是就有问题。
  • 测试仪的另一个功能就是测试电话线,将需测试的电话线两头分别接入测试仪两边,打开开关,两边的灯都是3-4灯亮,就表示测试的电话线正常。

实验材料

网线、网线钳、测线仪

实验步骤

网线的接法

  • 首先我们需要准备好我们连接网线水晶头的材料和工具:一根一定长度的网线、若干个水晶头、一把8P槽的压线钳、一个测试仪;
  • 步骤1、准备好工具和选择接法之后,第一步首先用压线钳将网线的胶皮剪掉,长度月2CM长即可,注意不要将网线的铜芯剪掉;
  • 步骤2、剪掉网线后,按照我们选择的接法和对应的标准,将网线按照对应标准的颜色进行缠绕、排序好;
  • 步骤3、将排序好的网线所有末端减齐,方便我们之后放进水晶头内;
  • 步骤4、将水晶头带塑料弹簧片的一端向下,带有金属针脚向上,把整齐的8股网线插入水晶头内,并使其紧紧的顶在顶端;
  • 步骤5、将水晶头放入8P的槽内,用力握紧压线钳节,最后水晶头就连接好了;

实验原始记录:步骤与现象

计网实验四

基本上就是几个命令啦

版本 1.0 by.dayi 最新更新:https://type.dayiyi.top/index.php/archives/124/

文件下载:

VLAN 之间不可以访问的:https://p.dabbit.net/blog/pic_bed/pkt_7215be82-428f-af3c-d555-28cfca635d61.pkt

VLAN 之间可以访问的:https://p.dabbit.net/blog/pic_bed/pkt_routing_f72bd405-7609-b525-f4c4-1345a06ce76e.pkt

基本配置1

找个交换机,找个电脑,先连起来

image-20230111150609851

基本命令

进入交换机CLI,记得先切英文输入法。

image-20230111150647906

  • 需要回到上个模式输入exit
  • 在任何模式下,用户在输入命令时,不用全部将其输入,只要前几个字母能够唯一标识该命令便可,或者在此时按Tab键将显示全称。

    en [按下TAB]
    enable [将会自动补全为这个]

    image-20230111150750443

    不一定能百分百触发,多试一试,然后回车就可

  • 在任何模式下,打一个“?”即可显示所有在该模式下的命令。

    ?
    [会出现下面的内容↓]
    Exec commands:
      <1-99>      Session number to resume
      auto        Exec level Automation
      clear       Reset functions
      clock       Manage the system clock
      configure   Enter configuration mode
      connect     Open a terminal connection
      copy        Copy from one file to another
      debug       Debugging functions (see also 'undebug')
      delete      Delete a file
      dir         List files on a filesystem
      disable     Turn off privileged commands
      disconnect  Disconnect an existing network connection
      enable      Turn on privileged commands
      erase       Erase a filesystem
      exit        Exit from the EXEC
      logout      Exit from the EXEC
      mkdir       Create new directory
      more        Display the contents of a f

image-20230111150908120

  • 如果不会拼写某个命令,可以键入开始的几个字母,在其后紧跟一个“?”,路由器即显示有什么样的命令与其匹配。

    pi?

    image-20230111151229914

    会出现这样的内容

    ping
  • 如不知道命令后面的参数应为什么,可以在该命令的关键字后空一个格,键入“?”,

    尽可能手动输入,这个复制好像不是很好用
    ping ?

    image-20230111151059636

  • 修改设备名

    enable #进入特权模式
    config #需要先进入配置模式,如果询问就回车
    hostname [设备名] #设置设备名
    no hostname #删除设备名

    image-20230111151546618

  • 显示交换机目前的的配置信息

    enable #进入特权模式
    show running-config 

    image-20230111153653325

    这个--more--可以通过 :

    • 按下q来退出
    • 一直按着回车 到结束

交换机的模式

这块本来想放在开头,想了想,还是放在这里吧,开头这么一大片可能难理解
模式长什么样子进入方法(样例)备注
普通用户模式switch0> 一直exit,然后回车
特权用户模式switch0#enable需要在 普通模式 下进入
全局配置模式switch0(config)#config 然后回车 或config tconfigure terminal需要在 特权模式 下进入
接口配置模式switch0(config-if)#interface port_ID 例:interface FastEthernet0/1需要在 全局配置模式 下进入
多接口配置模式switch0(config-if-range)#interface range fastethnet0/1-5interface range f0/1-10 需要在 全局配置模式 下进入
VLAN配置模式switch0(config-vlan)#vlan 1需要在 全局配置模式 下进入
退出当前模式-exit全局配置模式下:CTRL+Z也可以退出

image-20230111163059505

设置交换机密码

明文密码 (在全局配置模式下 switch0(config)#)

enable password 123456

image-20230111155330549

加密密码 (在全局配置模式下 switch0(config)#)

enable secret 12345678

image-20230111155343015

查看配置文件(在特权模式下Switch(config)#

exit #进入特权模式

image-20230111155531593

退出特权模式,尝试登录交换机

如果输错命令卡住可以等一等,也可以直接这样重启(但是可能丢失已经保存的配置)

image-20230111160019247

会强制重启(但是当前的配置文件可能会丢失

诶嘿,我发现个新办法

你多点几次这个,时间加速,好像就可以了

image-20230111161302531

exit
enable

image-20230111160118872

输入密码(密码字符不可见),这里是刚刚设置的 secret 的密码

image-20230111160337766

保存修改的配置

其实是将RAM 的信息保存到NVRAM,然后开机的时候加载这个配置,大概就是开机自启动的样子(开机跑脚本)

在特权模式下(#)

copy running-config startup-config

image-20230111160451272

登录提示信息

其实没啥用,也没有讲这个,觉得好玩可以试试

特权模式下(#)

banner motd "OH,that is a very big rabbit!" #每日的
banner login "ovo,hello" #登录的

image-20230111161102448

呃呃,好像刚刚登录的时候也不显示。可能得telnet的时候才显示?

保存个配置吧 在特权模式下(#)copy running-config startup-config

查看接口状态

特权模式

show interfaces fastEthernet 0/1
show interfaces 

image-20230111164634589

给交换机配置一个IP

二层交换机,二层就是数据链路层,IP是在第三层(网络层),所以想要配置IP,需要让交换机在第三层工作。

为了方便远程管理,大概给交换机配置一个IP进行远程访问吧

  1. 启动交换机的路由功能

    ip routing #启动交换机的路由功能

    image-20230111162830858

  2. 查看VLAN信息

    exit #进入特权模式(#)
    show vlan

    image-20230111162950831

    发现接口在VLAN 1下面

  3. 配置接口IP

    先进入配置模式(Switch(config)#):命令:conf

    然后输入下面的命令

    interface vlan 1
    ip add 192.168.233.254 255.255.255.0 #添加IP 192.168.233.254/24
    no shutdown #不关闭接口

image-20230111164508633

  1. 配置计算机IP

    一个网段就可以,这里配了192.168.233.2/24

    网关可以不用配

    image-20230111164855943

  2. 尝试ping交换机

    ping 192.168.233.254

    image-20230111165107671
    通了就可以了

  3. 保存交换机配置信息

    特权模式下(#):

    copy running-config startup-config

    image-20230111165337373

配置TELNET

  • 进入全局配置模式(Switch(config))

    Switch> enable 
    Switch# config terminal 
    Switch(config)#
  • 切换到vty线路

    Switch(config)# line vty 0 15
    • VTY = Virtual Terminal 也就是虚拟终端
    • 我们最多可以同时建立到交换机的16 个telnet 连接,这也是0 15的意思

    image-20230111170239537

  • 设置密码,并启用密码检查

    密码为:114514

    Switch(config-line)#password 114514
    Switch(config-line)#login

    image-20230111170401947

  • 尝试登录交换机

    在PC的终端输入

    telnet 192.168.233.254

    image-20230111170458754

    输入密码(不会显示字符)

    image-20230111170548982

  • 登录成功后:

    telnet命令的使用与交换机的CLI,没有什么差别

  • 记得保存交换机配置信息

    特权模式下(#):

    copy running-config startup-config

    image-20230111171831690

模拟真实场景下的VLAN配置

场景

大体如下

image-20230111172349233

配置IP后:

(网关不需要填写)

image-20230111173932457

问题分析

  • 用户访问服务器 192.168.233.52/24,经过了多个交换机
  • 每个交换机都进行了广播包的转发,同时ARP包也超级多

    image-20230111174051292

    image-20230111174121111

    image-20230111174209447

  • 当设备足够多的情况下,网络内的广播包非常非常大,有一些没有必要的包也会被每一个交换机转发,影响了网络的转发效率,严重情况下可能导致广播风暴。网络瘫痪等问题。
  • 为了避免这个问题,应该让一些不需要直接访问的网络进行隔开。避免广播包导致网络风暴。
  • 比如宾客的网络不需要与员工计算机进行互相访问,也不需要收到员工计算机的广播包。
  • 因此设置VLAN来缩小广播域。

划分VLAN

划分如下图:

  • VLAN 10: 绿色
  • VLAN 20: 橙色
  • VLAN 30: 蓝色(方便演示,新增滴)

image-20230111175840374

VLAN的种类

Access 模式:该模式下的端口只能连接单台设备,并且只能在该端口上传输一种类型的数据帧,例如只能传输 Ethernet 帧。

Hybrid 模式:该模式下的端口既可以连接单台设备,也可以连接多台设备,并且可以在该端口上传输多种类型的数据帧。

Trunk 模式:该模式下的端口可以连接多台设备,并且可以在该端口上传输多种类型的数据帧,主要用于连接不同 VLANs 之间的通信。

通常 Access 模式用于普通用户或设备端口,Trunk模式用于连接两个交换机之间。 Hybrid 模式具有 Access 和 Trunk 模式的部分功能,它主要用于满足一些特殊的应用场景。

配置交换机

这里根据接口来划分VLAN

进入交换机的CLI

image-20230111181055037

  • 进入全局配置模式

    enable
    conf t

    image-20230111181526847

  • 看好接口你滴接口可能和我的不一样

    我这里的接口如下:

    • vlan10 : Fa0/2 Fa0/4
    • vlan20:Fa0/1 Fa0/3
    • vlan30:Fa0/5

    有这样的对应关系,记得看好接口

  • 创建 VLAN 10

    vlan 10 
    name ovo #命名
    exit #退出

    image-20230111181958836

  • 创建VLAN 20

    vlan 20
    name owo_20 #命名
    exit #退出

    image-20230111182141819

  • 创建VLAN 30

    vlan 30
    name owo_30 #命名
    exit #退出

    image-20230111182146466

  • 分配:vlan10 : Fa0/2 Fa0/4

    interface range f0/2,f0/4
    switchport access vlan 10
    exit
    Switch(config)#interface range f0/2,f0/4
    Switch(config-if-range)#switchport access vlan 10

    image-20230111182441236

  • 分配:vlan20 : Fa0/1 Fa0/3

    interface range f0/1,f0/3
    switchport access vlan 20
    exit
  • 分配:vlan30 : Fa0/5

    interface range f0/5
    switchport access vlan 30
    exit

    image-20230111182923947

  • 上述的执行模式如文字信息

    Switch#conf 
    Configuring from terminal, memory, or network [terminal]? 
    Enter configuration commands, one per line.  End with CNTL/Z.
    Switch(config)#interface range f0/1,f0/3
    Switch(config-if-range)#switchport access vlan 20
    Switch(config-if-range)#exit
    Switch(config)#interface range f0/5
    Switch(config-if-range)#switchport access vlan 30
    Switch(config-if-range)#exit
    Switch(config)#
  • 查看VLAN分配信息

    在特权模式下(#):

    show vlan brief

    image-20230111183012009

    结果:

    Switch#show vlan brief
    
    VLAN Name                             Status    Ports
    ---- -------------------------------- --------- -------------------------------
    1    default                          active    Fa0/6, Fa0/7, Fa0/8, Fa0/9
                                                    Fa0/10, Fa0/11, Fa0/12, Fa0/13
                                                    Fa0/14, Fa0/15, Fa0/16, Fa0/17
                                                    Fa0/18, Fa0/19, Fa0/20, Fa0/21
                                                    Fa0/22, Fa0/23, Fa0/24, Gig0/1
                                                    Gig0/2
    10   ovo                              active    Fa0/2, Fa0/4
    20   owo_20                           active    Fa0/1, Fa0/3
    30   owo_30                           active    Fa0/5
    1002 fddi-default                     active    
    1003 token-ring-default               active    
    1004 fddinet-default                  active    
    1005 trnet-default                    active    
  • 可以看到正确的分配了VLAN
  • 保存配置信息

    特权模式下(#)

    copy running-config startup-config

    image-20230111183201581

测试不同VLAN是否会相互广播

在模拟模式下测试

image-20230111183240616

  • 使用VLAN 20的任意一台计算机,进入命令行

    arp -d #清除arp表
    ping 192.168.233.51

    image-20230111183351386

  • 发现VLAN之间的广播包(ARP包)

    没有出现相互广播的情况(广播域没有跨VLAN)

    image-20230111183517808

配置不同VLAN相互转发(也许不需要)

理论这个过程应该分别配置不同的子网,不然依然会有可能广播风暴。

在交换机3的CLI上

en
#进入config模式
config


#配置接口
interface range fastEthernet 0/1-5
switchport trunk encapsulation dot1q
switchport mode trunk 
Switch(config-if-range)#exit


#启动交换机的
Switch(config)#ip routing


#配置VLAN IP,如果子网不同应该配置多个IP
interface vlan 10
ip address 192.168.233.29 255.255.255.0
no shutdown
exit

interface vlan 20
no shutdown
exit


interface vlan 30
no shutdown
exit

exit
show ip route

回显

Switch(config-if)#interface vlan 10
Switch(config-if)#ip address 192.168.233.29 255.255.255.0
Switch(config-if)#no shutdown
Switch(config-if)#exit
Switch(config)#interface vlan 20
Switch(config-if)#
%LINK-5-CHANGED: Interface Vlan20, changed state to up

%LINEPROTO-5-UPDOWN: Line protocol on Interface Vlan20, changed state to up

Switch(config-if)#ip address 192.168.233.39 255.255.255.0
% 192.168.233.0 overlaps with Vlan10
Switch(config-if)#no shutdown
Switch(config-if)#exit
Switch(config)#interface vlan 30
Switch(config-if)#
%LINK-5-CHANGED: Interface Vlan30, changed state to up

%LINEPROTO-5-UPDOWN: Line protocol on Interface Vlan30, changed state to up

Switch(config-if)#ip address 192.168.233.109 255.255.255.0
% 192.168.233.0 overlaps with Vlan10
Switch(config-if)#no shutdown
Switch(config-if)#exit
Switch(config)#show ip route
                ^
% Invalid input detected at '^' marker.
    
Switch(config)#exit
Switch#
%SYS-5-CONFIG_I: Configured from console by console

Switch#show ip route
Codes: C - connected, S - static, I - IGRP, R - RIP, M - mobile, B - BGP
       D - EIGRP, EX - EIGRP external, O - OSPF, IA - OSPF inter area
       N1 - OSPF NSSA external type 1, N2 - OSPF NSSA external type 2
       E1 - OSPF external type 1, E2 - OSPF external type 2, E - EGP
       i - IS-IS, L1 - IS-IS level-1, L2 - IS-IS level-2, ia - IS-IS inter area
       * - candidate default, U - per-user static route, o - ODR
       P - periodic downloaded static route

Gateway of last resort is not set

C    192.168.233.0/24 is directly connected, Vlan10

配置完成后,可以两个VLAN进行访问

image-20230111185442681

文件下载

VLAN 之间不可以访问的:https://p.dabbit.net/blog/pic_bed/pkt_7215be82-428f-af3c-d555-28cfca635d61.pkt

VLAN 之间可以访问的:https://p.dabbit.net/blog/pic_bed/pkt_routing_f72bd405-7609-b525-f4c4-1345a06ce76e.pkt

Nginx Mixed_content 错误

代码里面写的不是很好,如果强制https访问,有些时候会打到http上,但是https不允许访问http,所以要进行修改

Nginx

添加

这里有个坑,如果你用了反代,需要在最底层的反代服务器上设置这个参数。

如果在表层的反代,似乎会替换掉这个参数。

add_header 'Content-Security-Policy' 'upgrade-insecure-requests';
server {
    # listen 80;
    listen 443 ssl;
    # error_page 497  https://$host$request_uri;
    
    #↓在这里设置没有用,得在被代的服务器上设置
    #add_header Content-Security-Policy "upgrade-insecure-requests;connect-src *";
    location / {
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header X-Forwarded-Proto https;
        add_header Content-Security-Policy upgrade-insecure-requests;

        proxy_pass http://host.docker.internal:8001;
        
    }
}

Html

在http头设置,好像还挺管用

<meta http-equiv="Content-Security-Policy" content="upgrade-insecure-requests"/>

计网实验3 子网掩码与划分子网

更新中:当前1.013,记得有空刷新网页。

主要的是理解这个概念,实操步骤其实很快很容易。

更新链接:https://type.dayiyi.top/index.php/archives/109/

文件下载

如果你实在配不动了,下这个文件参考一下即可,这是本文中已经配完的文件

实验中的pkt文件

https://cute-pic.cute.ac.cn/blog/pic_bed/net_3_by_dayi_a3e3cef2-764c-11ed-a1eb-0242ac120002.zip

https://p.dabbit.net/blog/pic_bed/net_3_by_dayi_a3e3cef2-764c-11ed-a1eb-0242ac120002.zip

IP地址

此处说的都是IPV4

  • IP(Internet Protocol Address)
  • 由4个字节,即32位二进制组成
  • 将每个字节分为一组,共分为4组
  • 也就是
  • 00000000.00000000.00000000.00000000

IPV4地址划分

目前网络绝大部分用的CIDR(无类别域间路由),已经不区分ABC类地址。但是就像原子是电子云,但是原子轨道仍有参考价值。

RFC1918对IP地址的分类 | 分类网络

A类IPv4地址B类IPv4地址C类IPv4地址
网络标志位010110
IP地址范围0.0.0.0~127.255.255.255128.0.0.0~191.255.255.255192.0.0.0~223.255.255.255
可用IP地址范围1.0.0.1~127.255.255.254128.0.0.1~191.255.255.254192.0.0.1~223.255.255.254
是否可以分配给主机使用
网络数量(个)126 (27-2)16384 (214)2097152 (221)
适用范围大量主机的大型网络中等规模主机数的网络小型局域网

image-20221208004634851

对于特殊的IP地址

网络号主机号是否可以作为源地址是否可以作为目的地址备注/描述
全为0全为0允许禁止表示本网主机
全为0Host ID允许禁止表示特定主机
全为1全为1禁止允许定向广播地址
127任意合法的值允许允许迂回地址,用于本地测试
Network ID全为1禁止允许直接广播地址

image-20221208004649831A类

  • 0.0.0.0 表示这个网络上的这个主机,如果你连的是互联网,这个就是全网的任意主机。可以这么感性理解,路由器和你在一个网络上,路由器把上的其他主机的信息发送给你,类似邮差,这样发给你。

D类

  • 网络标志位 1110
  • 224.0.0.0~239.255.255.255
  • 不可分配至主机,组播地址

E类

  • 网络标志位 11110
  • 240.0.0.0~247.255.255.255

广播地址

  • 255.255.255.255
  • 对应的二进制全为1是本地广播地址。
  • 广播地址同样的也是发送给FF:FF:FF:FF:FF:FF,交换机广播给所有的接口。

本地回环

  • 127.0.0.1-127.255.255.254

    访问到的都是自己,也叫作回环。

网络标志位

这是在二进制下的。

都是8位地址的开头两位或者三位

  • A类的开头:00...
  • B类的开头:10...
  • C类的开头:110...

比如

B类

00000000.00000000.00000000.00000000
10...... ........ ........ ........

↑这个
8位的二进制,如果第一位是1,后面的值再怎么变,最小值也是2^7=128了
由于是10
那样最大情况就是:
10111111 = 2^7+0*2^6+2^5+2^4+2^3+2^2+2^1+2^0 = 191

这样B类地址主机的范围就是128-191.xxx.xxx.yyy
其中xxx的范围是0-255
其中yyy的范围是1-254

C类

C类地址也同理

11000000.00000000.00000000.00000000
到
11011111.11111111.11111111.11111111

实际上就是这个IP段,但是对于全0的叫网络号,全1的叫广播

而
11011111
对应的十进制是223

image-20221207113010293

因此,我们可以得到

C类的地址的范围

192.0.0.0~223.255.255.255

网络号和主机号

来个奇怪的例子。

网络号相当于

  • 兔子猴子猫猫家园

主机号相当于

  • 1号楼304室
  • 章鱼哥的小屋子
  • 海绵宝宝家
  • 派大星的石头
  • 板栗猫的家
  • 板栗猫的电脑

如果我是一名开发商,我现在申请了猫猫家园的地皮(也就是网络号)

现在我可以去把每块地皮分配给不同的猫。(也就是主机号)

比如我申请了192.168.233.0/24这个IP地址作为猫猫家园的地皮

假设已经知道了,可以分的地皮是这个范围

  • 192.168.233.1
  • 192.168.233.2
  • ...
  • 192.168.233.253
  • 192.168.233.254

你把192.168.233.66分给了派大星的电脑

你把192.168.233.67分给了派大星的手机

你把192.168.233.6分给了板栗猫,虽然板栗猫有200多个设备,但是只能有一个设备在线。

你把192.168.233.10分给了章鱼哥的竖笛音乐服务器

你把192.168.233.12分给了章鱼哥的爪机

其中192.168.233.0叫做网络号也就是猫猫家园的网络号

而对于192.168.233.66 ,192.168.233.10这些IP中的,66,10就叫做主机号啦

如果在二进制下

192     .168     .233      .66    
11000000|10101000|1110 1001|00110110
<------------------------->|<------>
           网络号              主机号

前面这些是网络号,后面的就是主机号啦

子网掩码

上面的划分也引发出了第二个问题。

如何区分网络号和主机号。

这就是子网掩码要做的事情。

子网掩码类似于 IP 地址,但仅在网络内部使用。路由器使用子网掩码将数据包路由到正确的位置。在互联网上传输的数据包中并不含子网掩码——这些数据包仅指示目标 IP 地址,路由器会将其与子网进行匹配。

而对于分类网络:

分类网络是在是1993年之前,未引入CIDR在互联网,使用的一种网络架构架构。

在分类网络中|没有引入CIDR的时候

我们默认认为是这样:

其中x代表网络号,y代表主机号

A类的地址:
0.  0.  0.  0   = 00000000.00000000.00000000.00000000
127.255.255.255 = 01111111.11111111.11111111.11111111
                  0xxxxxxx.yyyyyyyy.yyyyyyyy.yyyyyyyy
B类的地址:    
128.  0.  0.  0 = 10000000.00000000.00000000.00000000
191.255.255.255 = 10111111.11111111.11111111.11111111
                  10xxxxxx.xxxxxxxx.yyyyyyyy.yyyyyyyy
    
C类的地址:  
192.  0.  0.  0 = 11000000.00000000.00000000.00000000
223.255.255.255 = 11011111.11111111.11111111.11111111
                = 110xxxxx.xxxxxxxx.xxxxxxxx.yyyyyyyy
   
分类前缀码开始地址结束地址对应子网掩码默认子网掩码
A类地址00.0.0.0127.255.255.255/8255.0.0.0
B类地址10128.0.0.0191.255.255.255/16255.255.0.0
C类地址110192.0.0.0223.255.255.255/24255.255.255.0
D类地址 (组播)1110224.0.0.0239.255.255.255/4未定义
E类地址 (保留)1111240.0.0.0255.255.255.255/4未定义

约定俗称,也就可以根据一个IP地址,就确定网络号主机号

但是,这样子,A类地址只能分给128个人

假设小兔子有一个服务器,于是申请了A类,但是分配的数量是255*255*255-2=16581375-2=16581373个IP地址。

这造成了很大的浪费,而且其他没有IP的猫猫,狗狗,分配不到IP,更别提网上冲浪了。

于是引入了CIDR

如果没有引入CIDR,也就是子网长度是不可变的。

子网掩码其实挺简单滴,把网络号的位数设置为1,主机号设置为0

192     .168     .233      .66    
11000000|10101000|11101001 |00110110
<------------------------->|<------>
           网络号              主机号
    
上面是一个C类的IP地址。

我们已经知道或者说要规定网络号是192.168.233.0/24

于是,子网掩码就是255.255.255.0

            192     .168     .233      .66    
IP地址:      11000000|10101000|11101001 |00110110
子网掩码:      11111111|11111111|11111111 |00000000 
            <------------------------->|<------>
                     网络号              主机号
而对应的十进制就是255.255.255.0

对于A类的地址同理

10.  123.  123.  0   = 00000000.00000000.00000000.00000000
255.   0.    0.  0   = 11111111.00000000.00000000.00000000
                       <------>|<------------------------->
                         网络号              主机号

引入CIDR

但是这样仍然不够,还是没有解决IP地址分配过剩的问题。

于是,引入CIDR,CIDR叫做无类别域间路由

核心是:可变长子网掩码(VLSM)

  • 可以指定任意长度的前缀的可变子网掩码。
  • 将多个连续的前缀聚合
  • 根据实际情况,而不是分类网络中固定的掩码来分配IP

而引入后,分类网络中的规定只是可以用来参考。

实际上完全可以忽视分类网络的规定。

举个例子

如果直观的感受一下,就是大概这个样子:

  • 255.255.255.0 对应的二进制是

    11111111.11111111.11111111.00000000 
    前面有24个1                   8个0
    
    假设有一个网络号
    网络号: 192.168.233.0
    子网掩码:255.255.255.0

    如果网络号为:192.168.233.0,则可以直接简单记为192.168.233.0/24 ,前面是网络号,后面是子网掩码长度。

  • 如果我们的实际需求不需要根据A类地址分配那么多主机,我们只需要分配240个左右即可

    那么可以这么做

    网络号:  10.  1. 1. 0
    子网掩码:255.255.255.0 对应就是/24
    
    二进制下是这个样子:
    网络号  : 00001010.00000001.00000001.00000000
    子网掩码: 11111111.11111111.11111111.00000000
    
    可以分配的IP
    10.1.1.1-254
    
    其中 10.1.1.0记录为网络号,254

    上面的例子中的CIDR方式的话,可以记作:10.1.1.0/24

  • 同样的,我也可以分配更多的主机:10.0.0.0/8

    10.0.0.0/8
    
    00001010.00000000.00000000.00000000 -> 10 .1.1.0 |网络号
    11111111.00000000.00000000.00000000 -> 255.0.0.0 |子网掩码,只有8位
  • 上面的都是常规的子网掩码的例子,/24 (255.255.255.0)/16 (255.255.0.0)/8 (255.0.0.0),比较符合十进制的认知,而下面如果不是这三个数,是否还是可以的呢?

    是可以的。

这似乎解决了一定的IP不够用的问题

我可以在分配的时候不用分配整个A类,我虽然分给猫猫的是A类的IP,但是实际上,我只给猫猫了一个/24,也就是只有254个可用IP。

但是似乎也没有完全解决。

板栗只有8个设备,我假设分配给他254个IP,就会有浪费。

非 常规地址位 的划分

目前我有一个IP段,192.168.233.0/24

我分给板栗IP,我只想给它分配14个IP,应该怎么做?

先给出答案:

我利用192.168.233.0/28,划分出16个子网。

每个子网分别是:

子网网段起始IP结束IP广播地址
192.168.233.0192.168.233.1192.168.233.14192.168.233.15
192.168.233.16192.168.233.17192.168.233.30192.168.233.31
192.168.233.32192.168.233.33192.168.233.46192.168.233.47
192.168.233.208192.168.233.209192.168.233.222192.168.233.223
192.168.233.224192.168.233.225192.168.233.238192.168.233.239
192.168.233.240192.168.233.241192.168.233.254192.168.233.255

image-20221208020404505

如果子网掩码前面有28个1,对应的子网掩码是255.255.255.240

我只需要要将其中的一个子网给板栗即可。

如果我是板栗猫

dayi给我了三个子网

第一个是 192.168.233.0/28

第二个是 192.168.233.16/28

第三个是 192.168.233.224/28

这三个子网都拥有16个IP地址,可以分配的地址有14个(除去一个网络号和广播IP)

上面并不好理解,但是如果再二进制下,还是挺好理解的。

在二进制下就是这样的:

 192     .168     .233     .1
 11000000.10101000.11101001.0000   0001 <-IP地址
 11111111.11111111.11111111.1111   0000 <-子网掩码
|<------------网络号------------>|<-主机号->       

可以看到,IP地址的后四位的主机号,是可以分配的。

总共有4位二进制,2x2x2x2=16(排列组合),正好可以分配16个IP,去掉一个广播IP:1111 = 15(192.168.233.15),去掉一个网络号:0000 = 0(192.168.233.0),正好有14个IP地址可以正常用滴

对于第二个子网:192.168.233.16/28

 192     .168     .233     .17
 11000000.10101000.11101001.0001   0001 <-IP地址
 11111111.11111111.11111111.1111   0000 <-子网掩码
|<------------网络号------------>|<-主机号->   

两者的差别也就是网络号中有差别,主机号仍然是4位二进制。

但是如果将这个东东转换到10进制,因为最后一位的网络号和主机号合起来计算为十进制,就导致了有些时候难以理解。同样可以分配的地址是:删掉一个广播IP1111192.168.233.31,0001|1111=31),删掉一个网络号IP 0000 ( 192.168.233.16 ,0001|0000=16)

同样的:

而对于第三个子网:192.168.233.224/28

 192     .168     .233     .225
 11000000.10101000.11101001.1110   0001 <-IP地址
 11111111.11111111.11111111.1111   0000 <-子网掩码
|<------------网络号------------>|<-主机号->      

广播IP:192.168.233.239 ,1110 1111 = 239

网络号:192.168.233.224 ,1110 0000 = 224

过程中可以用win10,win11的计算器调到程序员模式计算下二进制

image-20221207184611556

网关

网关是一个普通的IP地址,任何电脑都可以做网关。

但是网关还有一个邮递员的作用。

比如,当前网络是:192.168.233.0/24

我想访问 type.dayiyi.top 也就是这个IP39.105.181.66

当前网络里面的主机,我根据子网掩码算出,39.105.181.66,不是当前网络的IP,于是我告诉网关,兔子想网上冲浪。

于是网关收到信息,把想冲浪的消息发给网关的网关,一层一层的找(实际过程涉及BGP,挺麻烦滴),直到找到这个主机。然后再一层一层的返回数据,最后网关就把网页发给你啦!

操作内容分析

现要将一C类网段地址192.168.18.0/24分成四个子网(主机数为80,50,24)。若按固定子网掩码的方式来划分该C类IP地址,要将该IP划分成4个子网,需要使用8位主机号的三位(可划分2^3-2=6个子网),剩下的5位主机号最多只能容纳2^5-2=30台主机,不满足要求。

根据要求

  • 需要划分的子网段是192.168.18.0/24
  • 要分成4个子网
  • 前3个子网的主机数量为:80 50 24
  • 最后一个子网10台公共设备(根据原文后面推出)

如果均等划分,最后可以得到的IP数量不足

于是可以这么划分

  • 192.168.18.0/24 (可用:256台主机)

    • 192.168.18.0/25 (可用:126台主机 | 子网1:80台)
    • 192.168.18.128/25(可用:126台主机)

      • 192.168.18.128/26 (可用:62台主机|子网2:50台)
      • 192.168.18.192/26 (可用:62台主机)

        • 192.168.18.192/27 (可用:30台主机|子网3:24台)
        • 192.168.18.224/27 (可用:30台主机|子网4:10台)

这样就可以划分为4个子网啦

最后可以得到这个表格

子网网段起始IP结束IP广播地址CIDR子网掩码
子网1192.168.18.0192.168.18.1192.168.18.126192.168.18.127192.168.18.0/25255.255.255.128
子网2192.168.18.128192.168.18.129192.168.18.158192.168.18.159192.168.18.128/26255.255.255.192
子网3192.168.18.192192.168.18.193192.168.18.222192.168.18.22192.168.18.192/27255.255.255.224
子网4192.168.18.224192.168.18.225192.168.18.254192.168.18.255192.168.18.224/27255.255.255.224

image-20221207205652060

实验

配好第一个子网

信息

【子网1-主机数量80台】

  • CIDR:192.168.18.0/25
  • 起始IP:192.168.18.1
  • 终止IP:192.168.18.126
  • 网关:192.168.18.1
  • 子网掩码:255.255.255.128
  • 广播地址:192.168.18.127
  • 网络号:192.168.18.0

开配

  1. 找出交换机来,再找出4个电脑来

    image-20221207205030507

  2. 用这个auto线连起来

    image-20221207205114250

  3. 给他们配上IP

    第一个子网是: 192.168.18.0/25 对应的子网掩码是: 255.255.255.128 ,网关就配192.1688.18.1

    • 192.168.18.1 (给路由器留的IP,也作为网关啦,就临时不要用啦)
    • 192.168.18.2
    • 192.168.18.3
    • 192.168.18.4
  4. 还记得怎么配IP吗?

    image-20221207205819732

  5. 像这样:

    image-20221207205900429

  6. 这里可以改底色,画正方形哦

    image-20221207210325169

  7. 把几台电脑依次配好,大概最后是这样,你也可以随便标注一下哦

    image-20221207212657880

  8. 如何修改主机下面的文字?

    image-20221207214515027

  9. 尝试相互之间ping一下,都是互通滴

    image-20221207210955531

配好第二个子网

信息

第二个子网的信息如下:

【子网2-主机数量50台】

  • CIDR:192.168.18.128/26
  • 起始IP: 192.168.18.129
  • 终止IP:192.168.18.158
  • 子网掩码:255.255.255.192
  • 网关:192.168.18.129
  • 广播地址:192.168.18.159
  • 网络号:192.168.18.128

开配

  • IP的分配,这里网关就用192.168.18.129啦

    • 192.168.18.129 等会分给路由器,作为网关
    • 192.168.18.130-158 任意取哦
  • 子网掩码是:255.255.255.192,不要填错了哦

    image-20221207212927691

  • 配完之后大概这个样子!

    image-20221207213442125

  • 也可以测测相互通不通

    image-20221207213736390

配好第三个子网

信息

第三个子网的信息如下:

【子网3-主机数量24台】

  • CIDR:192.168.18.192/27
  • 起始IP: 192.168.18.193
  • 终止IP:192.168.18.222
  • 子网掩码: 255.255.255.224
  • 网关:192.168.18.193
  • 广播地址:192.168.18.223
  • 网络号:192.168.18.192

开配

其实就是重复性的工作啦

  • 分配IP

    • 192.168.18.193 作为路由器IP
    • 192.168.18.194-222 都可以分
  • 子网掩码

    255.255.255.224
  • 网关

    Q:可不可以不是这个IP?A:可以,但是大多数的网关都是最开始的地址或者最后一个地址
    192.168.18.193
  • 继续配就好啦

    image-20221207214313382

    image-20221207214653884

  • 当当

    image-20221207214927687

配好第四个子网

信息

第四个子网的信息如下:

【子网4-主机数量10台-访客】

  • CIDR:192.168.18.224/27
  • 起始IP: 192.168.18.225
  • 终止IP:192.168.18.254
  • 子网掩码: 255.255.255.224
  • 网关: 192.168.18.225
  • 广播地址:192.168.18.255
  • 网络号:192.168.18.224

开配

重复性的工作啦

  • 分配IP

    • 192.168.18.225 作为路由器IP
    • 192.168.18.226-254 都可以分
  • 子网掩码

    255.255.255.224
  • 网关

    192.168.18.225
  • 重复性滴工作

    image-20221207215244209

  • 当当

    image-20221207215708137

选择路由器

不要用交换机

  • 因为是四个网络,如果你用交换机作为核心的话,四个网络之间不会互通。

    • 如果进行抓包可以发现,ARP请求不会得到回应。

      image-20221207220220077

喵呀,老师的课上内容少一点,但是也可以做啦!

为了完成作业,下面内容变得相对于课上要复杂一些。

咱们也不弄的太复杂,BGP之类的就先不用啦,从简单说下原理。

就用静态路由寻路就可以。

路由器寻路

下面是理论环节,实际步骤可以继续往下翻即可

对于静态路由。

这是两个路由器,他们之间要想相互访问,就需要路由器在同一个网段下。

image-20221207230352968

假设,路由器0还接着,192.168.18.1这个ip

image-20221207230633271

如果192.168.18.0/24有请求来到路由器0,想继续访问192.168.18.226/27

我们通过拓扑图很容易可以看到,流量发给路由器 1即可。

但是,我们路由器不是AI,不是GPT3,他不知道包要发给谁。

这个时候我们就要手动配置路由表的下一跳。

image-20221207231326319

这样子就可以啦。

主线不相关:如何显示接口

  • ctrl+R

    image-20221207220848417

  • 选择这个

    image-20221207220920237

配置路由器信息

选2911路由器

因为2911有三个网口,可以省一点设备费(也省点人工费

要两个2911路由器。

路由器之间的IP地址

  • 路由器0:10.0.0.1/8
  • 路由器1:10.0.0.2/8

把路由器之间连起来

拿出两个路由器之后,用AUTO闪电线连起来。(你也可以在两个路由器之间放个交换机,不影响滴)

image-20221207232118709

配置路由器0的IP地址

  • 选择路由器
  • config->接口0/0->打开网口->填入IP 10.0.0.1/8

image-20221207232330041

配置路由器1的IP地址

  • 选择路由器
  • config -> 0/0 -> 打开->配置IP:10.0.0.1/8

image-20221207232643871

路由器 0 连入两个子网

注意,接口和网关要对应起来,这里的路由器0/1口对应的是子网1的网关IP。

你连的接口可能跟我的不一样(我的是0/1连到子网1,你可能是0/2连到子网1)

  • 用闪电线连接两个子网的交换机(如果闪电线连不上,就用黑色的线直接连就可以端口选fastethernet<->GigabitEtherbet就可以)
  • 记下连接的是路由器的哪个端口
  • 配置路由器的IP地址,路由器作为子网的网关

image-20221207233453832

配置接口0/1
  • IP地址:192.168.18.1
  • 子网掩码:255.255.255.128
  • 没错,路由器就是网关

image-20221207233631609

配置接口0/2
  • IP地址:192.168.18.129
  • 子网掩码:255.255.255.192

image-20221207233725019

路由器 1 连入两个子网

跟路由器0同理

实际的接口是1还是2请根据你连接的子网和端口来确定
配置接口0/1
  • IP地址:192.168.18.225
  • 子网掩码:255.255.255.224
配置接口0/2
  • IP地址:192.168.18.193
  • 子网掩码:255.255.255.224

image-20221207234035674

目前的链路图

image-20221207234320268

配置路由器的静态路由

路由器能够把连接到自身的网络进行相互转发
在路由器0配置静态路由
  • 我们知道以下两个网络,应该是通过路由器1转发的。

    • 192.168.18.224/27
    • 192.168.18.192/27
  • 路由器1在路由器之间的内网IP为:10.0.0.2/8
  • 于是我们希望访问224和192的网络的时候,流量转发到10.0.0.2即可。
  • 于是我们配置如下的静态路由

    • 静态路由1

      • 网络号:192.168.18.224
      • 子网掩码:255.255.255.224
      • 下一跳:10.0.0.2
    • 静态路由2

      • 网络号:192.168.18.192
      • 子网掩码:255.255.255.224
      • 下一跳:10.0.0.2
  • 配置方法入下:

    • 打开路由,进入config,添加静态路由

      image-20221207235202671

    • 如图,添加两个静态路由。

      image-20221207235244217

在路由器1配置静态路由

我们目前路由器0知道了哪些流量应该转发给1,但是1并不知道哪些流量应该转发给0

于是进行如下配置:

  • 打开路由器1
  • 添加两条静态路由

    • 静态路由1

      • 网络号:192.168.18.0
      • 子网掩码:255.255.255.128
      • 下一跳:10.0.0.1
    • 静态路由2

      • 网络号:192.168.18.128
      • 子网掩码:255.255.255.192 (注意这里跟上面不一样)
      • 下一跳:10.0.0.1
  • 关闭即可

    image-20221207235848351

至此,完成配置

最终网络拓扑图

image-20221208001956378

尝试互相访问网络

尝试用

  • 192.168.18.196

    • 访问192.168.18.228 (子网4)
    • 访问192.168.18.157 (子网2)
    • 访问192.168.18.4 (子网1)
    • 访问均可以互通,ping可以正常发包回包,表示双方均可以正常通信。

      image-20221208001924033

      image-20221208002046638

  • 192.168.18.4

    • 192.168.18.228 (子网4)
    • 192.168.18.196 (子网3)
    • 192.168.18.157 (子网2)
    • 10.0.0.2
    • 均可以互通

      image-20221208002545201

      image-20221208002708671

  • 路由追踪:

    • 192.168.18.227

      image-20221208002903154

      image-20221208002952839

文件下载

实验中的pkt文件

https://pic.icee.top/blog/pic_bed/net_3_by_dayi_a3e3cef2-764c-11ed-a1eb-0242ac120002.zip

实验结束。

数据结构与算法实验报告

|顺序表|顺序表的查找操作

1. 实训题目

本题要求实现一个函数,要求从顺序表中查找指定元素,并返回第一个查找成功的元素在表中的位置序号,若查找失败,则返回0;

函数接口定义:

int LocateElem(SqList L,ElemType e);

其中SqList结构定义如下:

typedef struct{
    ElemType *elem;
    int length;
 }SqList;

裁判测试程序样例:

#include <stdio.h>
#include <stdlib.h>
#define MAXSIZE 5
typedef int ElemType;
typedef struct{
    ElemType *elem;
    int length;
}SqList;
void InitList(SqList &L);/*细节在此不表*/
int LocateElem(SqList L,ElemType e);
int main(){
    SqList L;
    InitList(L);
    ElemType e;
    int p;
    scanf("%d",&e);
    p=LocateElem(L,e);
    printf("The position of %d in SequenceList L is %d.",e,p);
    return 0;
}
/* 请在这里填写答案 */

2. 算法思想

顺序表的遍历

3. 算法步骤

  • 因为是顺序表,所以,第一个指针指向0就可以。(也可以说指到表头)
  • 设置一个计数器i,初始化为0
  • 从第一个节点开始访问,访问到L.length-1的点。
  • 如果找到与元素e相同的元素,返回i+1,因为存储单位是从0开始的。

4. 源代码

int LocateElem(SqList L,ElemType e){
  for(int i=0;i<L.length;++i){
    if(L.elem[i]==e)return i+1;
  }
  return 0;
}

|顺序表|顺序表的插入操作

1.实训题目

本题要求实现一个函数,在顺序表的第i个位置插入一个新的数据元素e,插入成功后顺序表的长度加1,函数返回值为1;插入失败函数返回值为0;

函数接口定义:

int ListInsert(SqList &L,int i,ElemType e);

其中SqList结构定义如下:

typedef struct{
    ElemType *elem;
    int length;
}SqList;

裁判测试程序样例:

#include <stdio.h>
#include <stdlib.h>
#define MAXSIZE 5
typedef int ElemType;
typedef struct {
  ElemType *elem;
  int length;
} SqList;
void InitList(SqList &L); /*细节在此不表*/
int ListInsert(SqList &L, int i, ElemType e);
int main() {
  SqList L;
  InitList(L);
  ElemType e;
  int i;
  scanf("%d%d", &i, &e);
  int result = ListInsert(L, i, e);
  if (result == 0) {
    printf("Insertion Error.The value of i is unlawful or the storage space is "
           "full!");
  } else if (result == 1) {
    printf("Insertion Success.The elements of the SequenceList L are:");
    for (int j = 0; j < L.length; j++) {
      printf(" %d", L.elem[j]);
    }
  }
  return 0;
}
/* 请在这里填写答案 */

2.算法思想

顺序表的插入操作。

先将i后面的元素整体后移,然后插入元素和维护长度。

3.算法步骤

  • 存储的时候是以0开始存储的,所以开始的时候让i--,这样可以更加方便的写代码
  • 判断非法条件,i>=L.length i大于L的长度,i小于0L的长度已经到达最大值,都返回0
  • 设置计数器ii,初始化为L的长度-1
  • iiL的长度-1递减到ii,每次减1
  • 赋值:让L的第ii+1元素的值为第ii元素的值
  • 插入元素:修改第i个元素的值为e
  • 维护表长:L的长度+1
  • 返回状态

4.源代码

int ListInsert(SqList &L,int i,ElemType e){
  i=i-1;
  if(i>L.length||i<0)return 0;//大于长度
  if(L.length==MAXSIZE)return 0;
  for(int ii=L.length-1;ii>=i;--ii){//后移元素
      L.elem[ii+1]=L.elem[ii];
    }
  L.elem[i]=e;//修改元素
  L.length++;//修改表长
  return 1;
}

|顺序表|顺序表的删除操作

1.实训题目

本题要求实现一个函数,要求将顺序表的第i个元素删掉,成功删除返回1,否则返回0;

函数接口定义:

int ListDelete(SqList &L,int i);

其中SqList结构定义如下:

typedef struct{
    ElemType *elem;
    int length;
 }SqList;

裁判测试程序样例:

#include <stdio.h>
#include <stdlib.h>
#define MAXSIZE 5
typedef int ElemType;
typedef struct {
  ElemType *elem;
  int length;
} SqList;
void InitList(SqList &L); /*细节在此不表*/
int ListDelete(SqList &L, int i);
int main() {
  SqList L;
  InitList(L);
  int i;
  scanf("%d", &i);
  int result = ListDelete(L, i);
  if (result == 0) {
    printf("Delete Error.The value of i is illegal!");
  } else if (result == 1) {
    printf("Delete Success.The elements of the SequenceList L are:");
    for (int j = 0; j < L.length; j++) {
      printf(" %d", L.elem[j]);
    }
  }
  return 0;
}
/* 请在这里填写答案 */

2.算法思想

顺序表的删除操作。

将i后面的元素整体后移,然后维护长度。

3.算法步骤

  • 判断非法条件: 都返回0

    • i大于L的长度,
    • i小于等于0
    • L的长度已经到达最大值。
    • L的长度小于1
  • 设置计数器j,初始化为i-1
  • ji-1递增到L.elem-2,每+1
  • 赋值:让L的第j个元素的值为L的第j+1的元素的值
  • 维护表长:L.length -1
  • 返回状态值

4. 源代码

int ListDelete(SqList &L,int i){
  if(L.length<1||i>L.length||i<=0)return 0;
  for(int j=i-1;j<L.length-1;++j){//从i-1到L.elem-2 进行操作
    L.elem[j]=L.elem[j+1];
  }
  L.length-=1;//修改长度
  return 1;
}

|顺序表|求顺序表最大值

1.实训题目

本题要求实现一个函数,要求返回顺序表的最大值,空表返回0。题目保证顺序表中所有元素都为正整数。

函数接口定义:

int GetMax(SqList L);

其中SqList结构定义如下:

typedef struct{
    ElemType *elem;
    int length;
}SqList;

裁判测试程序样例:

#include <stdio.h>
#include <stdlib.h>
#define MAXSIZE 5
typedef int ElemType;
typedef struct{
    ElemType *elem;
    int length;
 }SqList;
void InitList(SqList &L);/*细节在此不表*/
int GetMax(SqList L);

int main()
{
    SqList L;
    InitList(L);
    int p;
    p=GetMax(L);
    if(p) printf("The max of SequenceList L is %d.\n",p);
    else printf("The SequenceList is null.\n");
    return 0;
}

/* 请在这里填写答案 */

2.算法思想

枚举顺序表中的每一个元素。

然后进行判断是否为当前的最大值

3.算法步骤

  • 判断非法条件: 都返回0

    • L的长度为0
  • 定义一个无限大的数cmax0x3f3f3f3f (7f7f7f7f容易爆int)
  • i0递增到L.length-1,每+1
  • 赋值:让cmaxcmaxL的第i个的元素的最大值
  • 返回cmax

4. 源代码

int max(int a,int b){return a>b?a:b;}
int GetMax(SqList L){
  if(L.length==0)return 0;
  int cmax=-0x3f3f3f3f;
  for(int i=0;i<L.length;++i){
    cmax=max(cmax,L.elem[i]);
  }
  return cmax;
}

|顺序表|后记

过程中用了这个调试代码来初始化和调试

void InitList(SqList &L){
  L.elem=new ElemType[MAXSIZE];//线性表标记到数组
  L.length = 0;//设置当前的长度为0
  for(int i=0;;++i){//读入
    ElemType e;
    scanf("%d",&e);
    if(e==-1)break;//如果是-1 break掉
    L.elem[L.length++]=e;//插入
  }
}

|链表|求单链表的表长

1.实训题目

本题要求实现一个函数,求带头结点的单链表的表长。

函数接口定义:

int Length ( LinkList L );

其中LinkList结构定义如下:

typedef struct LNode{
    ElemType data;
    struct LNode *next;
}LNode,*LinkList;

L是带头结点的单链表的头指针,函数Length返回单链表的长度。

裁判测试程序样例:

#include <stdio.h>
#include <stdlib.h>
typedef int ElemType;
typedef struct LNode {
  ElemType data;
  struct LNode *next;
} LNode, *LinkList;
LinkList Create(); /* 细节在此不表 */
int Length(LinkList L);
int main() {
  LinkList L = Create();
  printf("%d\n", Length(L));
  return 0;
}
/* 你的代码将被嵌在这里 */

输入样例:

2 1 4 5 3 -1

输出样例:

5

2. 算法思想

枚举每个节点,然后计数器自增。

3. 算法步骤

  • 申请一个临时变量siz作为计数器,并且初始化为0
  • iL-next开始,每次赋值为i->next
  • 每次循环进行加1
  • 返回siz

4. 源代码

int Length ( LinkList L ){
  int siz=0;
  for(LinkList i=L->next;i!=NULL;i=i->next){
    siz++;
  }
  return siz;
}

|链表|带头结点的单链表插入操作

1. 实训题目

6-2 带头结点的单链表插入操作

本题要求实现带头结点的单链表插入操作,插入成功返回1,否则返回0。

函数接口定义:

int insert_link ( LinkList L,int i,ElemType e);

L是单链表的头指针,i为插入位置,e是插入的数据元素,插入成功返回1,否则返回0。

裁判测试程序样例:

#include <stdio.h>
#include <stdlib.h>
typedef int ElemType;
typedef struct LNode {
  ElemType data;
  struct LNode *next;
} LNode, *LinkList;
LinkList Create(); /* 细节在此不表 */
void print(LinkList L);
int insert_link(LinkList L, int i, ElemType e);
int main() {
  int position, insert_data;
  int flag;
  LinkList L = Create();
  scanf("%d", &position);
  scanf("%d", &insert_data);
  flag = insert_link(L, position, insert_data);
  if (flag) {
    print(L);
  } else {
    printf("Wrong Position for Insertion");
  }
  return 0;
}
void print(LinkList L) {
  LinkList p;
  p = L->next;
  while (p) {
    printf("%d ", p->data);
    p = p->next;
  }
}
/* 请在这里填写答案 */

输入格式:

输入数据为三行,第一行是若干正整数,最后以-1表示结尾(-1不算在序列内,不要处理)。所有数据之间用空格分隔。
第二行数据是插入位置,第三行数据是被插入元素值。

输入样例:

1 2 3 4 5 6 -1
2 
100

输出样例:

1 100 2 3 4 5 6 

2.算法思想

判断非法条件之后,找到需要插入位置i前一个节点,新建节点进行插入,同时维护链表。

3.算法步骤

  • 先计算大小,方法如同求单链表的表长,申请变量siz,从j=L开始,每次赋值j=j->next:,每次循环siz自增。
  • 如果插入的坐标i小于0或者大于 链表的长度,则不是合法插入,返回0
  • 申请新的内存。
  • 初始化新的节点ovo
  • 设置循环变量j,初始化为j=L,终止条件j!=NULL,循环赋值j=j->next
  • 如果++cnt的值为i,也就是j的后继为待插入位置

    • 新建节点的后继设置为当前节点j的后继。 也就是执行ovo->next=j->next
    • 当前节点j的后继设置为申请的新的节点ovo

4.源代码

int insert_link(LinkList L, int i, ElemType e){
  int siz=0;
  for(LinkList j = L;j!=NULL;j=j->next){++siz;}//计算大小
  if(i<=0 || i>siz)return 0;//错误判断
  LNode *ovo = (LNode*)malloc(sizeof(LNode));//新建节点
  ovo->data=e;//赋值
  int cnt= 0;//计数器
  for(LinkList j = L;j!=NULL;j=j->next){
    if(++cnt == i){
      ovo->next=j->next;
      j->next=ovo;
    }
  }
  return 1;
}

|链表|带头结点的单链表删除操作

1. 实训题目

本题要求实现删除单链表的第i个元素结点,删除成功返回1,否则返回0。

函数接口定义:

int delete_link ( LinkList L,int i);

L为单链表的头指针,i为删除结点的序号。

裁判测试程序样例:

#include <stdio.h>
#include <stdlib.h>
typedef int ElemType;
typedef struct LNode {
  ElemType data;
  struct LNode *next;
} LNode, *LinkList;
LinkList Create(); /* 细节在此不表 */
void print(LinkList L);
int delete_link(LinkList L, int i);
int main() {
  LinkList L = Create();
  int position;
  int flag;
  scanf("%d", &position);
  flag = delete_link(L, position);
  if (flag) {
    print(L);
  } else {
    printf("Wrong Position for Deletion");
  }
  return 0;
}
void print(LinkList L) {
  LinkList p;
  p = L->next;
  while (p) {
    printf("%d ", p->data);
    p = p->next;
  }
}
/* 请在这里填写答案 */

输入格式:

输入数据为两行,第一行是若干正整数,最后以-1表示结尾(-1不算在序列内,不要处理)。所有数据之间用空格分隔。
第二行数据是删除位置。

输入样例:

1 2 3 4 5 6 -1
3

输出样例:

1 2 4 5 6 

2.算法思想

先判断非法条件。

枚举链表,找到该节点时,删除节点,维护链表结构。

3.算法步骤

  • 判断非法条件:如果插入位置大于链表长度,或者小于等于0则为非法
  • 设置计数器cnt,初始化为0
  • 设置循环变量j,初始化为j=L,终止条件j!=NULL,循环赋值j=j->next
  • cnt+1等于i的时候,即当元素,则开始插入

    • 建立临时变脸nx
    • 判断当前的j的下一个,是否是链表最后一个元素,如果是nx=NULL,否则让nx等于j的下一个的下一个,即要删除元素的后继。
    • 删除(j->next),释放内存空间,维护链表。
    • 只需要让j(待删除节点的前驱)指向待删除节点的后继,即为j->next=nx
  • 返回插入成功状态值。

4.源代码

int delete_link ( LinkList L,int i){
  int siz=0;
  for(LinkList j = L;j!=NULL;j=j->next){++siz;}//计算大小
  siz-=1;
  
  if(i>siz||i<=0)return 0;
  int cnt= 0;
  for(LinkList j = L;j!=NULL;j=j->next){
    if(++cnt==i){
      LinkList nx;
      if(j->next==NULL)nx=NULL;
      else nx=j->next->next;
      free(j->next);
      j->next=nx;
      break;
    }
  }
  return 1;
}

|链表|求单链表元素序号

1. 实训题目

求单链表元素序号

本题要求实现一个函数,求带头结点的单链表中元素序号。

函数接口定义:

int Locate ( LinkList L, ElemType e);

L是带头结点的单链表的头指针,e是要查找的元素值。如果e在单链表中存在,函数Locate返回其序号(序号从1开始);否则,返回0。

裁判测试程序样例:

#include <stdio.h>
#include <stdlib.h>
typedef int ElemType;
typedef struct LNode{
    ElemType data;
    struct LNode *next;
}LNode,*LinkList;
LinkList Create();/* 细节在此不表 */
int Locate ( LinkList L, ElemType e);
int main(){
    ElemType e;
    LinkList L = Create();
    scanf("%d",&e);
    printf("%d\n", Locate(L,e));
    return 0;
}
/* 你的代码将被嵌在这里 */

输入样例:

2 1 4 5 3 -1
5

输出样例:

4

2.算法思想

枚举结点,同时计数。

3.算法步骤

  • 设置变量cnt 初始化为0,用于计数
  • 设置循环变量i,初始化为i=L-next,终止条件i!=NULL,循环赋值i=i->next

    • 循环内 cnt 自增
    • 循环内进行判断,当前节点的data值是否为e

      • 如果为e 则返回cnt的值
  • 如果循环结束没有进行返回,则说明cnt不存在,返回0

4.源代码

int Locate(LinkList L, ElemType e){
  int cnt=0;
  for(LinkList i=L->next;i!=NULL;i=i->next){
    ++cnt;
    if(i->data==e){
      return cnt;
    }
  }
  return 0;
}

|链表|带头结点的单链表就地逆置

1. 实训题目

带头结点的单链表就地逆置

本题要求编写函数实现带头结点的单链线性表的就地逆置操作函数。L是一个带头结点的单链表,函数ListReverse_L(LinkList &L)要求在不新开辟节点的前提下将单链表中的元素进行逆置,如原单链表元素依次为1,2,3,4,则逆置后为4,3,2,1。

函数接口定义:

void ListReverse_L(LinkList &L);

其中 L 是一个带头结点的单链表。

裁判测试程序样例:

//库函数头文件包含
#include<stdio.h>
#include<malloc.h>
#include<stdlib.h>

//函数状态码定义
#define TRUE        1
#define FALSE       0
#define OK          1
#define ERROR       0
#define INFEASIBLE -1
#define OVERFLOW   -2

typedef int  Status;
typedef int  ElemType; //假设线性表中的元素均为整型

typedef struct LNode
{
    ElemType data;
    struct LNode *next;
}LNode,*LinkList;

Status ListCreate_L(LinkList &L,int n)
{
    LNode *rearPtr,*curPtr;   //一个尾指针,一个指向新节点的指针
    L=(LNode*)malloc(sizeof (LNode));
    if(!L)exit(OVERFLOW);
    L->next=NULL;               //先建立一个带头结点的单链表
    rearPtr=L;  //初始时头结点为尾节点,rearPtr指向尾巴节点
    for (int i=1;i<=n;i++){  //每次循环都开辟一个新节点,并把新节点拼到尾节点后
        curPtr=(LNode*)malloc(sizeof(LNode));//生成新结点
        if(!curPtr)exit(OVERFLOW);
        scanf("%d",&curPtr->data);//输入元素值
        curPtr->next=NULL;  //最后一个节点的next赋空
        rearPtr->next=curPtr;
        rearPtr=curPtr;
    }
    return OK;
}
void ListReverse_L(LinkList &L);
void ListPrint_L(LinkList &L){
//输出单链表
    LNode *p=L->next;  //p指向第一个元素结点
    while(p!=NULL)
    {
          if(p->next!=NULL)
               printf("%d ",p->data);
          else
               printf("%d",p->data);
          p=p->next;
    }
}
int main()
{
    LinkList L;
    int n;
    scanf("%d",&n);
    if(ListCreate_L(L,n)!= OK) {
          printf("表创建失败!!!\n");
          return -1;
    }
    ListReverse_L(L);
    ListPrint_L(L);
    return 0;
}
/* 请在这里填写答案 */

输入格式

第一行输入一个整数n,表示单链表中元素个数,接下来一行共n个整数,中间用空格隔开。

输出格式

输出逆置后顺序表的各个元素,两个元素之间用空格隔开,最后一个元素后面没有空格。

输入样例:

4
1 2 3 4

输出样例:

4 3 2 1

2.算法思想

新建一个链表,枚举旧链表的同时使用头插法进行插入新的链表。

3.算法步骤

  • 新建一个链表ls,申请内存空间
  • 初始化链表ls: ls->next =NULL; ls->data=0;
  • 设置循环变量i,初始化为i=L-next,终止条件i!=NULL,循环赋值i=i->next

    • 循环内:新建节点tmp,申请内存空间。

      • 循环内:tmp进行赋值,tmpdata=idata
      • 头插法:tmp的下一个等于ls的下一个。
      • 头插法:ls的下一个节点为tmp
  • L 赋值为新建的列表。
  • 此处可以把旧的链表的内存空间清空。

4.源代码

void ListReverse_L(LinkList &L) {
  LNode *ls = (LNode*)malloc(sizeof(LNode));
  ls->next=NULL;
  ls->data=0;
  for(LinkList i = L->next;i!=NULL;i=i->next){
    LNode *tmp = (LNode*)malloc(sizeof(LNode));
    tmp->data=i->data;
    tmp->next=ls->next;
    ls->next=tmp;
  }
  L=ls;
}

|链表|带头结点的链式表操作集

1. 实训题目

带头结点的链式表操作集

本题要求实现带头结点的链式表操作集。

函数接口定义:

List MakeEmpty(); 
Position Find( List L, ElementType X );
bool Insert( List L, ElementType X, Position P );
bool Delete( List L, Position P );

其中List结构定义如下:

typedef struct LNode *PtrToLNode;
struct LNode {
    ElementType Data;
    PtrToLNode Next;
};
typedef PtrToLNode Position;
typedef PtrToLNode List;

各个操作函数的定义为:

List MakeEmpty():创建并返回一个空的线性表;

Position Find( List L, ElementType X ):返回线性表中X的位置。若找不到则返回ERROR;

bool Insert( List L, ElementType X, Position P ):将X插入在位置P指向的结点之前,返回true。如果参数P指向非法位置,则打印“Wrong Position for Insertion”,返回false;

bool Delete( List L, Position P ):将位置P的元素删除并返回true。若参数P指向非法位置,则打印“Wrong Position for Deletion”并返回false。

裁判测试程序样例:

#include <stdio.h>
#include <stdlib.h>

#define ERROR NULL
typedef enum {false, true} bool;
typedef int ElementType;
typedef struct LNode *PtrToLNode;
struct LNode {
    ElementType Data;
    PtrToLNode Next;
};
typedef PtrToLNode Position;
typedef PtrToLNode List;

List MakeEmpty(); 
Position Find( List L, ElementType X );
bool Insert( List L, ElementType X, Position P );
bool Delete( List L, Position P );

int main()
{
    List L;
    ElementType X;
    Position P;
    int N;
    bool flag;

    L = MakeEmpty();
    scanf("%d", &N);
    while ( N-- ) {
        scanf("%d", &X);
        flag = Insert(L, X, L->Next);
        if ( flag==false ) printf("Wrong Answer\n");
    }
    scanf("%d", &N);
    while ( N-- ) {
        scanf("%d", &X);
        P = Find(L, X);
        if ( P == ERROR )
            printf("Finding Error: %d is not in.\n", X);
        else {
            flag = Delete(L, P);
            printf("%d is found and deleted.\n", X);
            if ( flag==false )
                printf("Wrong Answer.\n");
        }
    }
    flag = Insert(L, X, NULL);
    if ( flag==false ) printf("Wrong Answer\n");
    else
        printf("%d is inserted as the last element.\n", X);
    P = (Position)malloc(sizeof(struct LNode));
    flag = Insert(L, X, P);
    if ( flag==true ) printf("Wrong Answer\n");
    flag = Delete(L, P);
    if ( flag==true ) printf("Wrong Answer\n");
    for ( P=L->Next; P; P = P->Next ) printf("%d ", P->Data);
    return 0;
}
/* 你的代码将被嵌在这里 */

输入样例:

6
12 2 4 87 10 2
4
2 12 87 5

输出样例:

2 is found and deleted.
12 is found and deleted.
87 is found and deleted.
Finding Error: 5 is not in.
5 is inserted as the last element.
Wrong Position for Insertion
Wrong Position for Deletion
10 4 2 5 

2.算法思想

  • 函数MakeEmpty

    • 生成节点,生成指针,指针指向节点。
  • 函数 Find

    • 枚举节点,找到相同数据则返回
  • 函数 Insert

    • 枚举节点,找到位置的时候,新建节点,维护链表
  • 函数 Delete

    • 枚举节点,找到节点并且删除,维护链表。

3.算法步骤

MakeEmpty 函数

  • 新建一个节点nd
  • 初始化节点ndnd的下一节点指向空,数值初始化为0
  • 新建指针ptn,指向nd
  • 新建List lsList 也是指针,所以直接等于ptn即可。
  • 返回列表ls

Find 函数

  • 新建循环变量ii初始化为L,循环终止条件:i!=NULL,每次循环i = i->Next
  • 如果发现Data的值为X,返回当前节点,即为i,返回i
  • 如果循环结束仍然没有找到,返回ERROR

Insert 函数

  • 设置循环变量i,初始化为i=L,终止条件i!=NULL,循环赋值i=i->next

    • 如果i的下一个节点为P,则进行

      • 新建节点nd
      • nd的数值初始化为X
      • i的后继设置为nd
      • nd的后继设置为P
      • 返回 true 状态值
  • 如果没有找到节点:

    • "Wrong Position for Insertion\n"
    • 返回false状态值

Delete 函数

  • 设置循环变量i,初始化为i=L,终止条件i!=NULL,循环赋值i=i->next
  • 如果i的后继为P

    • i的后继设置为P的后继
    • P免费了(free(P) 释放内存空间)。
    • 返回true状态值
  • 如果没有找到P

    • 输出"Wrong Position for Deletion\n“
    • 返回false状态值

4.源代码

List MakeEmpty(){
  //struct LNode nd;
  struct LNode ovo;
  struct LNode *nd = (struct LNode*)malloc(sizeof(struct LNode));
  nd->Next=NULL;
  nd->Data=0;
  PtrToLNode ptn = (PtrToLNode)malloc(sizeof(PtrToLNode));
  ptn = nd;
  List ls = (List)malloc(sizeof(List));
  ls=ptn;
  ls->Next=NULL;
  return ls;
}
Position Find(List L, ElementType X){
  for(PtrToLNode i = L;i!=NULL;i=i->Next){
    if(i->Data==X){
      return i;
    }
  }
  return ERROR;
}

bool Insert(List L, ElementType X, Position P){
  for(PtrToLNode i=L;i!=NULL;i=i->Next){
    if(i->Next == P){
      struct LNode *nd = (struct LNode*)malloc(sizeof(struct LNode));
      nd->Data=X;
      i->Next=nd;//i -> nd
      nd->Next=P;// nd ->p
      return true;
    }
  }
  printf("Wrong Position for Insertion\n");
  return false;
}

bool Delete(List L, Position P){
  for(PtrToLNode i=L;i!=NULL;i=i->Next){
    if(i->Next==P){
      i->Next=P->Next;
      free(P);
      return true;
    }
  }
  printf("Wrong Position for Deletion\n");
  return false;
}

|树与二叉树|统计二叉树叶子结点

1.实训题目

统计二叉树叶子结点个数

本题要求实现一个函数,可统计二叉树的叶子结点个数。

函数接口定义:

int LeafCount ( BiTree T);

T是二叉树树根指针,函数LeafCount返回二叉树中叶子结点个数,若树为空,则返回0。

裁判测试程序样例:

#include <stdio.h>
#include <stdlib.h>
typedef char ElemType;
typedef struct BiTNode{
    ElemType data;
    struct BiTNode *lchild,*rchild;
}BiTNode,*BiTree;
BiTree Create();/* 细节在此不表 */
int LeafCount ( BiTree T);
int main(){
    BiTree T = Create();
    printf("%d\n", LeafCount(T));
    return 0;
}
/* 你的代码将被嵌在这里 */

输入样例:

输入为由字母和'#'组成的字符串,代表二叉树的扩展先序序列。例如对于如下二叉树,输入数据:

AB#DF##G##C##

输出样例(对于图中给出的树):

二叉树.png

3

2. 算法思想

递归遍历树,如果是叶子结点,返回1,每层递归统计,返回和。

3. 算法步骤

  • 如果遍历到空结点NULL,则返回0
  • 当前层的计数器初始化为0
  • 如果当前节点的左子树和右子树的值都为空,则返回1
  • 其他情况:递归到左子树,然后递归到右子树。
  • 返回当前节点的计数器ans给上层节点。

4. 源代码

int LeafCount(BiTree T) {
  if(T==NULL){return 0;}
  int ans = 0 ;
  if(T->lchild==NULL&&T->rchild==NULL){return 1;}
  ans+=LeafCount(T->lchild);
  ans+=LeafCount(T->rchild);
  return ans;
}

|树与二叉树|二叉树的三种遍历(先序、中序和后序)

1.实训题目

本题要求实现给定的二叉树的三种遍历。

函数接口定义:

void Preorder(BiTree T);
void Inorder(BiTree T);
void Postorder(BiTree T);

T是二叉树树根指针,Preorder、Inorder和Postorder分别输出给定二叉树的先序、中序和后序遍历序列,格式为一个空格跟着一个字符。

其中BinTree结构定义如下:

typedef char ElemType;
typedef struct BiTNode
{
   ElemType data;
   struct BiTNode *lchild, *rchild;
}BiTNode, *BiTree;

裁判测试程序样例:

#include <stdio.h>
#include <stdlib.h>
typedef char ElemType;
typedef struct BiTNode
{
   ElemType data;
   struct BiTNode *lchild, *rchild;
}BiTNode, *BiTree;

BiTree Create();/* 细节在此不表 */
void Preorder(BiTree T);
void Inorder(BiTree T);
void Postorder(BiTree T);
int main()
{
   BiTree T = Create();
   printf("Preorder:");   Preorder(T);   printf("\n");
   printf("Inorder:");    Inorder(T);    printf("\n");
   printf("Postorder:");  Postorder(T);  printf("\n");
   return 0;
}
/* 你的代码将被嵌在这里 */

输入样例:

输入为由字母和'#'组成的字符串,代表二叉树的扩展先序序列。例如对于如下二叉树,输入数据:

AB#DF##G##C##

输出样例(对于图中给出的树):

二叉树.png

Preorder: A B D F G C
Inorder: B F D G A C
Postorder: F G D B C A

2. 算法思想

按照不同顺序遍历二叉树。

前序遍历:根 左 右

中序遍历:左 右 根

后序遍历:左 右 根

3. 算法步骤

  • 如果当前节点是NULL节点,则返回
  • 对于先序遍历

    • 输出当前节点(也就是根)T->data
    • 遍历左子树
    • 遍历右子树
  • 对于中序遍历

    • 遍历左子树 T->lchild
    • 输出当前节点(根节点) T->data
    • 遍历右子树 T->rchild
  • 对于后序遍历

    • 遍历左子树 T->lchild
    • 遍历右子树 T->rchild
    • 输出当前节点(根节点) T->data

4. 源代码

void Preorder(BiTree T){
  if(T==NULL)return;
  printf(" %c",T->data);
  Preorder(T->lchild);
  Preorder(T->rchild);
}
void Inorder(BiTree T){
  if(T==NULL)return;
  Inorder(T->lchild);
  printf(" %c",T->data);
  Inorder(T->rchild);
}
void Postorder(BiTree T){
  if(T==NULL)return;
  Postorder(T->lchild);
  Postorder(T->rchild);
  printf(" %c",T->data);
}

|树与二叉树|求二叉树的深度

1.实训题目

求二叉树的深度

本题要求实现一个函数,可返回二叉树的深度。

函数接口定义:

int Depth(BiTree T);

T是二叉树树根指针,函数Depth返回二叉树的深度,若树为空,返回0。

裁判测试程序样例:

#include <stdio.h>
#include <stdlib.h>
typedef char ElemType;
typedef struct BiTNode {
  ElemType data;
  struct BiTNode *lchild, *rchild;
} BiTNode, *BiTree;
BiTree Create(); /* 细节在此不表 */
int Depth(BiTree T);
int main() {
  BiTree T = Create();
  printf("%d\n", Depth(T));
  return 0;
}
/* 你的代码将被嵌在这里 */

输入样例:

输入为由字母和'#'组成的字符串,代表二叉树的扩展先序序列。例如对于如下二叉树,输入数据:

AB#DF##G##C##

输出样例(对于图中给出的树):

二叉树.png

4

2. 算法思想

遍历二叉树,每层向上返回:当前层找到的最大深度+1,进而求出深度。

3. 算法步骤

  • 如果当前节点为空:NULL 返回 0
  • 当前节点可获得的最大深度为:

    • 左子树获得的最大深度+1
    • 右子树获得的最大深度+1
    • 两者取最大值,即位当前节点的最大深度。
    • Depth(T->lchild)+1 Depth(T->rchild)+1 的最大值
  • 返回当前节点的深度。

4. 源代码

int max(int x,int y){return x>y?x:y;}
int Depth(BiTree T){
  if(T==NULL)return 0;
  int dep = max(Depth(T->lchild)+1,Depth(T->rchild)+1);
  return dep;
}

|树与二叉树| 中序输出度为1的结点

1.实训题目

本题要求实现一个函数,按照中序遍历的顺序输出给定二叉树中度为1的结点。

函数接口定义:

void InorderPrintNodes( BiTree T);

T是二叉树树根指针,InorderPrintNodes按照中序遍历的顺序输出给定二叉树T中度为1的结点,格式为一个空格跟着一个字符。

其中BiTree结构定义如下:

typedef struct BiTNode
{
    ElemType data;
    struct BiTNode *lchild,*rchild;
}BiTNode,*BiTree;

裁判测试程序样例:

#include <stdio.h>
#include <stdlib.h>
typedef char ElemType;
typedef struct BiTNode
{
    ElemType data;
    struct BiTNode *lchild,*rchild;
}BiTNode,*BiTree;

BiTree Create();/* 细节在此不表 */
void InorderPrintNodes( BiTree T);
int main()
{
    BiTree T = Create();
    printf("Nodes are:");
    InorderPrintNodes(T);
    return 0;
}
/* 你的代码将被嵌在这里 */

输入样例:

输入为由字母和'#'组成的字符串,代表二叉树的扩展先序序列。例如对于如下二叉树,输入数据:

ACG#H###BEF###D##

输出样例(对于图中给出的树):

二叉树.PNG

Nodes are: G C E

2. 算法思想

按照题目要求,先序遍历二叉树,如果当前节点的出度为1,则输出当前节点。

3. 算法步骤

  • 如果当前节点为:NULL 则返回
  • 根据先序遍历,遍历左子树
  • 如果当前节点的出度之和是1,则输出:

    • T的左子树不等于空,结果是0或者1
    • T的右子树不等于空,结果是0或者1
    • 取两者的和
    • 当且仅当:左子树为空右子树不为空,或者,右子树为空左子树不为空 。此时的和为1
    • 则进行输出

4. 源代码

void InorderPrintNodes( BiTree T){
  if(T==NULL)return ;
  InorderPrintNodes(T->lchild);
  if((T->lchild!=NULL) +  (T->rchild!=NULL) == 1){
    printf(" %c",T->data);
  }
  InorderPrintNodes(T->rchild);
}

|树与二叉树|后记|调试代码与建树

因为OI的习惯,实在习惯不了在PTA在线打代码和这样子调试,运行超级慢,效率超级低。

还是希望可以在本地运行和调试代码。

于是题目虽然是函数题,但是其实也可以补全。

于是按照要求,建了棵树。

能用递归写的,其实用循环写也没问题,就是得多个状态栈,所以还是直接用递归建树啦。

有一个稍微坑点的地方是:读入根节点data的问题,如果读入了一次,递归到子树再读一遍,读入流指针就可能不对劲啦。

想了想跟回退读入流指针相比,还是递归的时候传一个上一次读到的节点。

void build_tree(BiTree T,char last){
  T->data=last;
  T->lchild=NULL;
  T->rchild=NULL;
  char l_data,r_data;
  scanf("%c",&l_data);
  if(l_data!='#'){
    BiTree tmp = (BiTree)malloc(sizeof(BiTNode));
    tmp->data=l_data;
    tmp->lchild=NULL;
    tmp->rchild=NULL;
    T->lchild=tmp;
    build_tree(T->lchild,l_data);
  }
  scanf("%c",&r_data);
  if(r_data!='#'){
    BiTree tmp=(BiTree)malloc(sizeof(BiTNode));
    tmp->data=r_data;
    tmp->lchild=NULL;
    tmp->rchild=NULL;
    T->rchild=tmp;
    build_tree(T->rchild,r_data);
  }
}
BiTree Create(){
  BiTree tree = (BiTree)malloc(sizeof(BiTNode));
  char root;
  scanf("%c",&root);//读入根节点
  tree->data = root;
  build_tree(tree,root);
  return tree;
}

写完这两行字,我就意识到,不需要传这个last变量,只需要在开始的时候读一下根节点,其他的都不需要特殊处理。

这样就是下面的代码啦

void build_tree(BiTree T){
  // T->data=last;
  T->lchild=NULL;
  T->rchild=NULL;
  char l_data,r_data;
  scanf("%c",&l_data);
  if(l_data!='#'){
    BiTree tmp = (BiTree)malloc(sizeof(BiTNode));
    tmp->data=l_data;
    tmp->lchild=NULL;
    tmp->rchild=NULL;
    T->lchild=tmp;
    build_tree(T->lchild);
  }
  scanf("%c",&r_data);
  if(r_data!='#'){
    BiTree tmp=(BiTree)malloc(sizeof(BiTNode));
    tmp->data=r_data;
    tmp->lchild=NULL;
    tmp->rchild=NULL;
    T->rchild=tmp;
    build_tree(T->rchild);
  }
}

BiTree Create(){
  BiTree tree = (BiTree)malloc(sizeof(BiTNode));
  char root;
  scanf("%c",&root);//读入根节点
  tree->data = root;
  build_tree(tree);
  return tree;
}

|栈、队列、串、数组、广义表|手写栈

1.实训题目

手写一个栈

2.算法思想

使用数组来表示栈,定义一个数表示指针。

3.算法步骤

  • sz数组存储栈元素
  • top存储栈顶位置在哪
  • get_top()函数

    • 返回栈顶元素,由于top表示栈顶,也就是sz[top]
  • pop()函数

    • 弹出元素,实际上就是直接让top-1即可。
  • empty()函数

    • 判断栈是否为空。
    • 如果top小于等于0,则说明已经弹出完成或者过度弹出,返回0
    • top大于等于1,说明栈中还有元素
  • push(int x)函数

    • top指针自增
    • 然后元素赋值到sz[top]

4.源代码

#include<stdio.h>

#define maxn 12345

struct stack{
  int sz[maxn];
  int top;
  stack(){//结构体构造函数,初始化。
    top=0;
  }
  int get_top(){return sz[top];}//返回栈顶元素
  bool pop(){//弹出栈顶元素。
    if(top<=0){
      return false;
    }
    top--;
    return true; 
  }
  int empty(){//栈是否为空
    if(top>=1)return 0;
    return 1;
  }
  void push(int x){//向栈添加元素
    sz[++top]=x;
  }
}st;

int main(){
  for(int i=1;i<=5;++i){
    st.push(i);
  }
  for(;!st.empty();){
    printf("%d ",st.get_top());
    st.pop();
  }
  for(int i=6;i<=10;++i){
    st.push(i);
  }
  for(;!st.empty();){
    printf("%d ",st.get_top());
    st.pop();
  }
}

5. 代码输出

5 4 3 2 1 10 9 8 7 6

|栈、队列、串、数组、广义表|手写队列

1.实训题目

手写非循环队列

2.算法思想

设置一个头指针,设置一个尾指针。

分别为指向队列的头和尾。

3.算法步骤

  • sz 数组保存数据
  • head 表示队列头
  • tail 表示队列的尾
  • 函数 queue() 构造函数

    • head = 1
    • tail = 1
    • 让两者都指向第一个元素。
  • 函数 front() 返回队列的头

    • head指向队列的头,所以直接返回sz[head]即可
  • 函数 pop() 弹出元素

    • head 自增即可。
    • 但这样的缺点是,之前的空间被浪费了,无法再次利用。
    • 解决这个问题可以直接队列和链表相结合,这样就可以释放不需要的空间。也可以使用循环队列。
  • 函数 empty() 判断是否为空

    • 只需要判断头(head)是否大于尾巴(tail)即可。

      • 如果头大于等于尾巴则说明已经为空
      • 不然不为空
  • 函数 push(int x) 向队列添加元素

    • 尾巴自增(tail++)
    • 向尾巴指向的数组的位置修改为x

4.源代码

#include<stdio.h>
#define maxn 23333
struct queue{//手写非循环队列
  int sz[maxn<<1];
  int tail;
  int head;
  queue(){
    head = 1;
    tail = 1;
  }
  int front(){
    return sz[head];
  }
  void pop(){
    head++;
    return; 
  }
  int empty(){
    if(head>=tail)return 1;
    return 0;
  }
  void push(int x){
    sz[tail++]=x;
  }
};

int main(){
  struct queue q;
  for(int i=1;i<=10;++i)q.push(i);
  for(;!q.empty();){
    printf("%d ",q.front());
    q.pop();
  }
}

5.代码输出

1 2 3 4 5 6 7 8 9 10

|栈、队列、串、数组、广义表|KMP

1.实训题目

pic

2.算法思想

KMP数组记录模式串匹配失败之后的模式串的前缀后缀最大的相同的位置

而在匹配过程中,避免多余的重复匹配,匹配失败后,直接跳到最大的按位相等的地方即可。

3.算法步骤

  • kmp数组的求解:

    • kmp[i]记录的是,当模式串匹配到第i位的时候失败了,进行自我跳转。
    • 从第二个字符开始自我匹配(此处数组从下标1开始存储),如果自我匹配失败,则进行自我跳回。直到跳回到开始的位置或者再次相等的地方。
    • 如果自我匹配成功,则可以让j指针自增,同时kmp数组的第i位,即当前位置,设置为j
  • KMP匹配

    • 求出模式串和主串的长度分别为lenlen_p
    • 匹配主串,如果匹配失败,直接通过j指针的跳转到kmp[j],回退。
    • 如果相等则j指针自增,直到发现j指针的大小与模式串的长度相同,则此处匹配成功。
  • 复杂度分析:

    • 观察kmp数组的求出过程后,可以发现,while语句几乎是在几跳中就彻底完成,时间基本上不会占用,因此实现基本上是O(N+与M一次方相关的数)
    • 具体的分析大概如下:
    • 来自山东OI选手rqy:每次位置指针i++,匹配失败的指针j最多增加一次,所以j最多增加len次,所以是O(N的长度+M的长度)=O(N+M)

4.源代码

#include<bits/stdc++.h>
const int maxn=1e6+8;

char s1[maxn];
char s2[maxn];

int kmp[maxn];

void getnext(){
  int j=0;
  //kmp[0]=kmp[1]=0;
  int len = strlen(s2+1);
  for(int i=2;i<=len;++i){
    while(j!=0 && s2[i]!=s2[j+1])j=kmp[j];
    if(s2[i]==s2[j+1])j++;
    kmp[i]=j;//赋值
  }
}
void KMP(){
  int j =0;
  int len=strlen(s1+1);
  int len_p=strlen(s2+1);
  for(int i=1;i<=len;++i){
    while(j&&s1[i]!=s2[j+1])j=kmp[j];
    if(s1[i]==s2[j+1])j++;
    if(j==len_p){
      printf("%d\n",i-len_p+1);
    }
  }
}

int main(){
  using namespace std;
  cin>>s1+1;
  cin>>s2+1;
  getnext();
  KMP();
  int len=strlen(s2+1);
  for(int i=1;i<=len;++i)printf("%d ",kmp[i]);
}
//洛谷通过
//第一次写KMP(之前manacher懂了KMP也没能会写,多少有些参考)

5.代码输出

pic

|栈、队列、串、数组、广义表|递归广义表

1.实训题目

E = (a,E)

2.算法思想

元素。自己指向自己

3.算法步骤

  • 申请个内存
  • 自己指向自己。(ovo->ptr=ovo)
  • data赋值( ovo->data=233)
  • 枚举元素i作为指针:初始化为:ovo,终止条件i!=NULL,循环赋值i=i->ptr

    • 输出信息

4.源代码

#include<stdlib.h>
#include<stdio.h>
typedef struct Node{
  int data;
  struct Node *ptr;
}nd,*point;
int main(){
  point ovo = (point)malloc(sizeof(Node));
  ovo->ptr=ovo;
  ovo->data=233;
  for(point i = ovo;i!=NULL;i=i->ptr){
    printf("[data:%d ptr:%x]->",i->data,i->ptr);
  }
}

5.代码输出

[data:233 ptr:781400]->[data:233 ptr:781400]->[data:233 ptr:781400]->[data:233 ptr:781400]->[data:233 ptr:781400]->[data:233 ptr:781400]->[data:233 ptr:781400]->[data:233 ptr:781400]->[data:233 ptr:781400]->[data:233 ptr:781400]->[data:233 ptr:781400]->[data:233 ptr:781400]->[data:233 ptr:781400]->[data:233 ptr:781400]->[data:233 ptr:781400]->[data:233 ptr:781400]->[data:233 ptr:781400]->[data:233 ptr:781400]->[data:233 ptr:781400]->[data:233 ptr:781400]->[data:233 ptr:781400]->[data:233 ptr:781400]->[data:233 ptr:781400]->[data:233 ptr:781400]->[data:233 ptr:781400]->[data:233 ptr:781400]->[data:233 ptr:781400]->[data:233 ptr:781400]->[data:233 ptr:781400]->[data:233 ptr:781400]->[data:233 ptr:781400]->[data:233 ptr:781400]->[data:233 ptr:781400]->[data:233 ptr:781400]->[data:233 ptr:781400]->[data:233 ptr:781400]->[data:233 ptr:781400]->[data:233 ptr:781400]->[data:233 ptr:781400]->[data:233 ptr:781400]->[data:233 ptr:781400]->[data:233 ptr:781400]....

|图论|图的基本概念与表示

1.实训题目

请根据下图给出:

(1)每个顶点的度

(2)邻接矩阵

(3)邻接表

图片1.png

2. 算法思想

邻接表,建表,然后统计每个点的出度。

图可表示为

8
a b 15
a c 2
a d 12
b e 6
c e 8 
c f 4
d g 3
e g 9
f g 10
f d 5
g b 4

通过网站可以再次二次确认一下图是否正确:

https://csacademy.com/app/graph_editor/

image-20221201184359220

如果进行数字字母映射,可以得到这个表

t1.in

7 11
1 2 15
1 3 2
1 4 12
2 5 6
3 5 8
3 6 4
4 7 3
5 7 9
6 7 10
6 4 5
7 2 4

3. 算法步骤

先建立一个邻接表,fst数组存链表头,Edge结构体存链表。

Edge结构体内包含:u,v,w,nxt 分别是边的起点u,终点v,权值w,和链表的下一条边。

cnt作为edge的指针,表示边的序号。

因为是有向图,所以开的边最好是两倍。

N 为 点的数量

M 为 边的数量

inline x map_fun(x)函数

inline char map_fun(int x){return 'a'+x-1;}//这俩好用
inline int map_fun(char x){return x-'a'+1;}//这俩好用

两个函数,分别实现了,将数字映射到字符 和 将字符映射到数字两个功能。

具体实现做法:

  • 数字 -> 字符 :

    • 'a'的值(65) 一,然后加上偏移量x
  • 字符->数字:

    • 偏移量减去基础偏移'a'(65)

addedge(int u,int v,int w)函数

也是链表的实现方法。

使用的是头插法进行。

这部分因为OI时期写习惯了,就用int作为指针啦。

传入的参数有,u,v,w

  • 首先让cnt自增。
  • 然后分别进行赋值edge[cnt].u =u,edge[cnt].v= v edge[cnt].w= w
  • 当前节点的后继指针设置为 u 点的链表头,也就是fst[u]
  • 让u点的链表头指针,指向当前节点,也就是cnt。

这样就完成了边的添加。

inline void addedge_mat(int u,int v,int w) 函数

邻接矩阵。传入的参数有u,v,w

只需要matrix[u][v]=w;

建立边即可。

inline int get_in_deg(int u)函数

求入度,这里用了最暴力的方式来计算,也就是枚举所有的边。

然后进行统计。

先枚举所有的节点

  • 计数器ans初始化为0
  • i为循环变量,i1->N,每次i进行自增。

    • j为循环变量初始化为fst[i],循环条件:jj!=0),每次循环进行赋值j = edge[j].nxt即为:从链表头开始,每次赋值为后继,直到无法找到下一个后继为止

      • 过程中,uu 设置为当前边的 u 即为edge[j].uvv设置为当前边的v即为edge[j].vww则为edge[j].w也就是当前边的权值。
      • 如果发现当前边指向u 也就是 vv=u,则计算边权和。
  • 最后输出边权和ans

其实入度和出度可以在邻接表建立的时候进行维护,没必要在这里求。

inline int get_out_deg(int u) 函数

  • 跟上一个基本上是同理滴。u为要计算出度的节点。
  • 初始化计数器变量ans 设置为0
  • i为循环变量初始化为fst[u],循环条件:ii!=0),每次循环进行赋值i = edge[i].nxt即为:从链表头开始,每次赋值为后继,直到无法找到下一个后继为止

    • 统计当前边的边权,加到ans变量内。
  • 返回ans变量作为答案

addedge函数内记录出入度

在维护邻接表之后,添加下面两行即可。

in_deg[v]+=w;
out_deg[u]+=w;

分别让v的入度加w

让u的出度加w

添加这两个函数即可,这样就可以O(1)的时间复杂度求出入度和出度。

inline int get_ind_o1(int u){return in_deg[u];}
inline int get_outd_o1(int u){return out_deg[u];}

4. 源代码

#include<iostream>
#include<cstdio>

char map[]={-1,'a','b','c','d','e','f','g','h'};//用于查对应关系
//1号节点对应着a;2号节点对应着b
inline char map_fun(int x){return 'a'+x-1;}
inline int map_fun(char x){return x-'a'+1;}
int M;
int N;
const int maxn=1234;
struct Edge{
  int u,v,w;
  int nxt;
}edge[maxn<<1];
int cnt =0;
int fst[maxn];
int in_deg[maxn];
int out_deg[maxn];

inline void addedge(int u,int v, int w){//感觉是默写出来的
  edge[++cnt].u=u,edge[cnt].v=v,edge[cnt].w=w;
  edge[cnt].nxt=fst[u];
  fst[u]=cnt;
  //下面两行维护入度和出度
  in_deg[v]+=w;
  out_deg[u]+=w;
}


inline int get_out_deg(int u){//暴力求出度
  int ans = 0;
  for(int i=fst[u];i;i=edge[i].nxt){
    ans+=edge[i].w;
  }
  return ans;
}
inline int get_in_deg(int u){//暴力求入度
  int ans= 0;
  for(int i=1;i<=N;++i){
    for(int j=fst[i];j;j=edge[j].nxt){
      int uu = edge[j].u;
      int vv = edge[j].v;
      int ww = edge[j].w;
      if(vv==u)ans+=ww;
    }
  }
  return ans;
}

inline int get_ind_o1(int u){return in_deg[u];}//O1求出度和入度
inline int get_outd_o1(int u){return out_deg[u];}

int matrix[11][11];
inline void addedge_mat(int u,int v,int w){//邻接矩阵
  matrix[u][v]=w;
}
inline void show_mat(){//打印邻接矩阵,并且好看一点
  printf("-------------mat-------------\n");
  for(int j=0;j<=N;++j){
    printf("%5c ",map_fun(j));
  }
  printf("\n");
  for(int i=1;i<=N;++i){
    printf("%5c ",map_fun(i));
    for(int j=1;j<=N;++j){
      printf("%5d ",matrix[i][j]);
    }
    printf("\n");
  }
  printf("-------------mat-------------\n\n");
}

inline void show_edge(){//打印邻接表,并且好看一点
  for(int i=1;i<=N;++i){
    printf("%c:",map_fun(i));
    for(int u = fst[i];u;u=edge[u].nxt){
      // if(u==fst[i])printf("[first]")
      int uu=edge[u].u;
      int vv=edge[u].v;
      int ww=edge[u].w;
      int nxt=edge[u].nxt;
      printf("->[u:%d v:%d w:%d nxt:%d]",uu,vv,ww,nxt);
    }
    printf("\n");
  }
}

int main(){
  using namespace std;
  freopen("t1.in","r",stdin);//读入文件
  
  cin>>N>>M;
  for(int i=1;i<=M;++i){
    int u,v,w;
    cin>>u>>v>>w;//读入边
    addedge(u,v,w);//邻接表
    addedge_mat(u,v,w);//邻接矩阵
  }
  show_mat();//显示邻接矩阵

  //计算入度,出度
  int sum_in_deg=0;
  int sum_out_deg=0;
  for(int i=1;i<=N;++i){//计算出度和入度和和,并且打印。用两种方法计算,答案一样。
    sum_in_deg+=get_in_deg(i);
    sum_out_deg+=get_out_deg(i);
    printf("%c out_deg: %d in_deg:%d deg:%d\n",map_fun(i),get_out_deg(i),get_in_deg(i),get_ind_o1(i)+get_outd_o1(i));
  }
  printf("sum_in_deg:%d sum_out_deg:%d\n",sum_in_deg,sum_out_deg);
  //
  printf("\n-------------edge-------------\n");
  show_edge();//打印邻接矩阵
  printf("-------------edge-------------\n");
}

5. 代码输出

-------------mat-------------
    `     a     b     c     d     e     f     g
    a     0    15     2    12     0     0     0
    b     0     0     0     0     6     0     0
    c     0     0     0     0     8     4     0
    d     0     0     0     0     0     0     3
    e     0     0     0     0     0     0     9
    f     0     0     0     5     0     0    10
    g     0     4     0     0     0     0     0
-------------mat-------------

a out_deg: 29 in_deg:0 deg:29
b out_deg: 6 in_deg:19 deg:25
c out_deg: 12 in_deg:2 deg:14
d out_deg: 3 in_deg:17 deg:20
e out_deg: 9 in_deg:14 deg:23
f out_deg: 15 in_deg:4 deg:19
g out_deg: 4 in_deg:22 deg:26
sum_in_deg:78 sum_out_deg:78

-------------edge-------------
a:->[u:1 v:4 w:12 nxt:2]->[u:1 v:3 w:2 nxt:1]->[u:1 v:2 w:15 nxt:0]
b:->[u:2 v:5 w:6 nxt:0]
c:->[u:3 v:6 w:4 nxt:5]->[u:3 v:5 w:8 nxt:0]
d:->[u:4 v:7 w:3 nxt:0]
e:->[u:5 v:7 w:9 nxt:0]
f:->[u:6 v:4 w:5 nxt:9]->[u:6 v:7 w:10 nxt:0]
g:->[u:7 v:2 w:4 nxt:0]
-------------edge-------------

应该不用手画图了吧,代码都是自己写的。

|图论|图的存储结构+遍历序列

1.实训题目

8-2 图的存储结构+遍历序列

设有下列无向图:
(1)画出该图的邻接矩阵。(3分)
(2)画出该图的邻接表。(3分)
(3)从V1出发,按照(1)中存储结构,写出深度优先遍历序列(3分);
(4)从V1出发,按照(1)中存储结构,写出广度优先遍历序列(3分);

t4.png

2. 算法思想

邻接表邻接矩阵跟上一个题目一样啦。

分别进行深度优先和广度优先遍历就好啦。

深度的话dfs到头就好。

广度的话用一个队列作为辅助即可。

题目转换

t2.in

6 10
1 2
1 3 
1 4
2 3
2 5
3 4
3 5
3 6
4 6
5 6

pic

3. 算法步骤

  • 邻接矩阵和邻接表的遍历和输出同上面一样啦。

    • 邻接矩阵 打印二维数组
    • 邻接表 遍历每个点,然后遍历链表的每个节点就可以啦
  • DFS 遍历

    • vis 数组记录的是当前节点是否已经遍历过,避免循环遍历。
    • 遍历当前节点的所有边,如果指向的边没有被遍历过,则进行遍历。

      • 起始条件:i = fst[u],当i==0的时候终止,每次循环:i=edge[i].nxt
      • 如果目标点没有被遍历过,则标记该节点已经遍历了,递归遍历目标节点dfs(vv)
  • BFS 遍历(代码中遍历了两次,一次用了STL的队列,一次用的手写的简单的队列)

    • 定义一个队列q
    • 函数传参u,将u加入到队列中。
    • 循环:直到队列为空

      • 从队列中取出元素now_u
      • 输出当前节点。
      • 遍历now_u的所有边,如果指向的节点没有被访问到,则加入到队列中。
      • 不断循环此过程,直到队列为空为止。

4. 源代码

#include<bits/stdc++.h>
int M;
int N;
const int maxn=1234;
struct Edge{
  int u,v,w;
  int nxt;
}edge[maxn<<1];
int cnt =0;
int fst[maxn];
int in_deg[maxn];
int out_deg[maxn];
inline void addedge(int u,int v, int w){//感觉是默写出来的
  edge[++cnt].u=u,edge[cnt].v=v,edge[cnt].w=w;
  edge[cnt].nxt=fst[u];
  fst[u]=cnt;
}

int matrix[11][11];
inline void addedge_mat(int u,int v,int w){//邻接矩阵
  matrix[u][v]=w;
  matrix[v][u]=w;
}


inline void show_mat(){//打印邻接矩阵,并且好看一点
  printf("-------------mat_start-------------\n");
  for(int j=0;j<=N;++j){//打印横着的坐标
    printf("v%d    ",j);
  }
  printf("\n");//换行
  for(int i=1;i<=N;++i){
    printf("v%d %5",i);//竖着的坐标
    for(int j=1;j<=N;++j){
      printf("%5d ",matrix[i][j]);//矩阵数据
    }
    printf("\n");
  }
  printf("-------------mat_end-------------\n\n");
}

inline void show_edge(){//打印邻接表,并且好看一点
  printf("-------------edge_start-------------\n");
  for(int i=1;i<=N;++i){
    printf("v%d:",i);
    for(int u = fst[i];u;u=edge[u].nxt){
      int uu=edge[u].u;
      int vv=edge[u].v;
      int ww=edge[u].w;
      int nxt=edge[u].nxt;
      printf("->[u:%d v:%d w:%d nxt:%d]",uu,vv,ww,nxt);
    }
    printf("\n");
  }
  printf("-------------edge_end-------------\n");
}

struct queue2{//非循环队列
  int sz[maxn<<1];
  int tail;
  int head;
  queue2(){
    head = 1;
    tail = 1;
  }
  int front(){
    return sz[head];
  }
  void pop(){
    head++;
    return; 
  }
  int empty(){
    if(head>=tail)return 1;
    return 0;
  }
  void push(int x){
    sz[tail++]=x;
  }
};

int vis[maxn];
void dfs(int u){
  if(!vis[u]){
    printf("[v%d]",u);
    vis[u]=1;
  }else{
    printf("->[v%d]",u);
  }
  

  for(int i=fst[u];i;i=edge[i].nxt){
    int uu = edge[i].u;
    int vv = edge[i].v;
    if(!vis[vv]){
      vis[vv]=1;
      dfs(vv);
    }
  }
}


void bfs(int u){
  std::queue <int> q;
  q.push(u);
  for(;!q.empty();){
    int now_u = q.front();
    q.pop();
    vis[now_u]=1;
    printf("[v%d]",now_u);
    for(int i = fst[now_u];i;i=edge[i].nxt){
      int uu = edge[i].u;
      int vv = edge[i].v;
      // printf("de:%d->%d\n",uu,vv);
      if(!vis[vv]){
        vis[vv]=1;
        q.push(vv);
      }
    }
  }
}



void bfs2(int u){
  queue2 q;
  q.push(u);
  for(;!q.empty();){
    int now_u = q.front();
    q.pop();
    vis[now_u]=1;
    printf("[v%d]",now_u);
    for(int i = fst[now_u];i;i=edge[i].nxt){
      int uu = edge[i].u;
      int vv = edge[i].v;
      // printf("de:%d->%d\n",uu,vv);
      if(!vis[vv]){
        vis[vv]=1;
        q.push(vv);
      }
    }
  }
}


int main(){
  freopen("t2.in","r",stdin);
  using namespace std;
  cin>>N>>M;//读入节点和边
  for(int i=1;i<=M;++i){//读入边
    int u,v,w;
    cin>>u>>v;
    addedge(u,v,1);//无向图两次加边
    addedge(v,u,1);
    addedge_mat(u,v,1);
  }

  show_mat();
  show_edge();

  printf("\n------------dfs_start-------------\n");
  for(int i=1;i<=N;++i)vis[i]=0;//清空数组。
  dfs(1);
  printf("\n-------------dfs_end-------------\n");

  printf("\n------------bfs_start-------------\n");
  for(int i=1;i<=N;++i)vis[i]=0;//清空数组。
  bfs(1);//STL 队列
  printf("\n------------bfs_end-------------\n");


  printf("\n------------bfs_start-------------\n");
  for(int i=1;i<=N;++i)vis[i]=0;//清空数组。
  bfs2(1);//手写队列 非循环队列
  printf("\n------------bfs_end-------------\n");

}

程序输出

-------------mat_start-------------
v0    v1    v2    v3    v4    v5    v6
v1     0     1     1     1     0     0
v2     1     0     1     0     1     0
v3     1     1     0     1     1     1
v4     1     0     1     0     0     1
v5     0     1     1     0     0     1
v6     0     0     1     1     1     0
-------------mat_end-------------

-------------edge_start-------------
v1:->[u:1 v:4 w:1 nxt:3]->[u:1 v:3 w:1 nxt:1]->[u:1 v:2 w:1 nxt:0]
v2:->[u:2 v:5 w:1 nxt:7]->[u:2 v:3 w:1 nxt:2]->[u:2 v:1 w:1 nxt:0]
v3:->[u:3 v:6 w:1 nxt:13]->[u:3 v:5 w:1 nxt:11]->[u:3 v:4 w:1 nxt:8]->[u:3 v:2 w:1 nxt:4]->[u:3 v:1 w:1 nxt:0]
v4:->[u:4 v:6 w:1 nxt:12]->[u:4 v:3 w:1 nxt:6]->[u:4 v:1 w:1 nxt:0]
v5:->[u:5 v:6 w:1 nxt:14]->[u:5 v:3 w:1 nxt:10]->[u:5 v:2 w:1 nxt:0]
v6:->[u:6 v:5 w:1 nxt:18]->[u:6 v:4 w:1 nxt:16]->[u:6 v:3 w:1 nxt:0]
-------------edge_end-------------

------------dfs_start-------------
[v1]->[v4]->[v6]->[v5]->[v3]->[v2]
-------------dfs_end-------------

------------bfs_start-------------
[v1][v4][v3][v2][v6][v5]
------------bfs_end-------------

------------bfs_start-------------
[v1][v4][v3][v2][v6][v5]
------------bfs_end-------------