C语言进阶——数据结构之链表

前言

hello,大家好呀,我是Humble  在之前的两篇博客,我们学完了数据结构中的顺序表,还对它进行了一个应用,做了一个通讯录的小项目

那今天我们再来学习一个新的数据结构——链表

b90abe6962934bd1880f3d53f5b63113.jpg

引入

我们来回忆一下顺序表

对于顺序表,我们发现它有下面的这些问题

1.中间/头部的插入删除,时间复杂度为O(N)
2.增容需要申请新空间,拷贝数据,释放旧空间。会有不小的消耗
3.增容一般是呈2倍的增长,势必会有一定的空间浪费。例如当前容量为100,满了以后增容到
200,我们再继续插入了5个数据,后面没有数据插入了,那么就浪费了95个数据空间

思考:如何解决以上问题呢?有没有以一种数据结构,它可以解决顺序表的这些问题呢?

这就是我们今天要讲的链表
 

链表的概念及结构

链表在物理存储结构上是非连续、非顺序的存储的、

其数据元素的逻辑顺序是通过链表中的指针链接次序实现的

而与顺序表不同的是,链表是由节点组成的
节点的组成主要有两个部分:

1.当前节点要保存的数据

2.保存下一个节点的地址(指针变量)
 

变量来保存下一个节点位置才能从当前节点找到下一个节点


结合结构体的知识,我们可以给出每个节点对应的结构体代码:
 

struct SListNode
{
int data; //节点数据,我们假设当前保存的节点为整型
struct SListNode* next; //指针变量⽤保存下⼀个节点的地址
};

当我们想要保存一个整型数据时,实际是向操作系统申请了一块内存,这个内存不仅要保存整型数
据,也需要保存下一个节点的地址


所以,当我们想要从第一个节点走到最后一个节点时,只需要在前一个节点拿上下一 个节点的地址就可以了(有点绕,请耐心理解哦)

那么,给定的链表结构中,我们来实现一下节点从头到尾的打印吧~

我们在创建一个SList 的工程表示单链表

然后创建3个文件,分别是我们的SList.h 头文件 ,SList.c源文件以及测试文件test.c

(这个大家应该已经很熟悉了吧)

在三个文件中,我们分别去实现各自的职能

SList.h

#pragma once


typedef int SLTDataType;

typedef struct SListNode
{
	SLTDataType data;
	struct SListNode* next;
}SLTNode;


void SLTPrint(SLTNode* phead);//打印

SList.c

#include"SList.h"


void SLTPrint(SLTNode* phead)
{
	SLTNode* pcur = phead;
	while (pcur)
	{
		printf("%d->", pcur->data);
		pcur = pcur->next;
	}
	printf("NULL\n");
}

test.c

#include "SList.h"


void SlistTest01() {
	//一般我们不会这样去创建链表,这里只是为了给大家展示链表的打印
	SLTNode* node1 = (SLTNode*)malloc(sizeof(SLTNode));
	node1->data = 1;
	SLTNode* node2 = (SLTNode*)malloc(sizeof(SLTNode));
	node2->data = 2;
	SLTNode* node3 = (SLTNode*)malloc(sizeof(SLTNode));
	node3->data = 3;
	SLTNode* node4 = (SLTNode*)malloc(sizeof(SLTNode));
	node4->data = 4;

	node1->next = node2;
	node2->next = node3;
	node3->next = node4;
	node4->next = NULL;  

	SLTNode* plist = node1;
	SLTPrint(plist);  //打印1->2->3->4->NULL
}

int main()
{


    SlistTest01();
	return 0;
}

我们来测试一下,按照我们的想法,应该打印1->2->3->4->NULL

运行结果:


 

单链表的实现

找到了链表的打印,我们就来实现链表的各个功能吧

链表的尾插

这要分两种情况来讨论

1.链表不为空

2.链表为空

先画张图来辅助理解一下:

假设我们要在链表插入 元素4

下面我们来写尾插STLPushBack的代码:

void SLTPushBack(SLTNode** pphead, SLTDataType x) //注意这里pphead是二级指针,用**
{
	assert(pphead);

    SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
    newnode->data = x;
    newnode->next = NULL;

	//链表为空,新节点作为phead
	if (*pphead == NULL) {
		*pphead = newnode;
		return;
	}
	//链表不为空,找尾节点
	SLTNode* ptail = *pphead;
	while ((ptail->next) != NULL) //遍历
	{
		ptail = ptail->next;
	}
	//遍历完之后ptail就是尾节点
	ptail->next = newnode; //完成尾插
}

下面我们来测试一下

我们在test.c中这样写:

void SlistTest02()
{
	SLTNode* plist = NULL;
	SLTPushBack(&plist, 1); //我们要把plist指针的地址传过去,这个很重要!
	
	SLTPrint(plist); //预计结果1->NULL
}

int main()
{


	SlistTest02();
		

	return 0;
}

运行一下:

当然,因为我们下面的操作都要设计申请节点,每次都要写:

   SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
    newnode->data = x;
    newnode->next = NULL;

我们干脆就再写一个函数,之后直接调用就行

这样代码就会变成这样

SLTNode* SLTBuyNode(SLTDataType x) //申请新节点
{
	SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
	newnode->data = x;
	newnode->next = NULL;

	return newnode;
	
}


void SLTPushBack(SLTNode** pphead, SLTDataType x) 
{
	assert(pphead);

	SLTNode* newnode = SLTBuyNode(x);

	//链表为空,新节点作为phead
	if (*pphead == NULL) {
		*pphead = newnode;
		return;
	}
	//链表不为空,找尾节点
	SLTNode* ptail = *pphead;
	while (ptail->next)
	{
		ptail = ptail->next;
	}
	//ptail就是尾节点
	ptail->next = newnode;
}

接下来我们来看一下头插SLTPushFront:

它同样分2种情况,但它们的代码是一样的,所以就不用分了

void SLTPushFront(SLTNode** pphead, SLTDataType x) 
{
	assert(pphead);
	SLTNode* newnode = SLTBuyNode(x);

	
	newnode->next = *pphead;
	*pphead = newnode;
}

测试一下:
 

void SlistTest02()
{
	SLTNode* plist = NULL;
	SLTPushBack(&plist, 1);
	SLTPushBack(&plist, 2);
	SLTPushBack(&plist, 3);
	SLTPushBack(&plist, 4);
	
	

	SLTPushFront(&plist, 5);          
	SLTPushFront(&plist, 6);        
	SLTPushFront(&plist, 7);
	SLTPrint(plist);         //期望结果为:7->6->5->1->2->3->4->NULL
}

int main()
{


	SlistTest02();
		

	return 0;
}

运行结果如下:

接下来看一下尾部删除SLTPopBack吧~

既然要删除,我们要保证链表不为空,所以相比前面的这几种操作,它还要加上

assert(*pphead);//表示链表不能为空

此外,要分链表是否只有一个节点,即是否有前驱节点这2中情况
 

void SLTPopBack(SLTNode** pphead) 
{
	assert(pphead);
	
	assert(*pphead);//保证链表不能为空

	
	//链表只有一个节点
	if ((*pphead)->next == NULL) 
	{
		free(*pphead);
		*pphead = NULL;
		return;
	}
     //链表有多个节点
	SLTNode* ptail = *pphead;
	SLTNode* prev = NULL;
	while ((ptail->next)!=NULL)
	{
		prev = ptail;
		ptail = ptail->next;
	}

	prev->next = NULL;
	//销毁尾结点
	free(ptail);
	ptail = NULL;
}

我们也来测试一下:

void SlistTest02()
{
	SLTNode* plist = NULL;
	SLTPushBack(&plist, 1);
	SLTPushBack(&plist, 2);
	SLTPushBack(&plist, 3);
	SLTPushBack(&plist, 4);
	
	


	SLTPopBack(&plist);
	SLTPrint(plist);  //预期结果为1->2->3->NULL
	
}


int main()
{


	SlistTest02();
		

	return 0;
}

运行结果如下:

接下来看一下头部删除SLTPopFront吧~

这个也很简单,我们直接上代码~

//头删
void SLTPopFront(SLTNode** pphead) 
{
	assert(pphead);
	//链表不能为空
	assert(*pphead);

	//让第二个节点成为新的头
	//把旧的头结点释放掉
	SLTNode* next = (*pphead)->next;
	free(*pphead);
	*pphead = next;
}

接下来我们也是测试一下

void SlistTest03()
{
	SLTNode* plist = NULL;
	SLTPushBack(&plist, 1);
	SLTPushBack(&plist, 2);
	SLTPushBack(&plist, 3);
	SLTPushBack(&plist, 4);

		//头删
	SLTPopFront(&plist);
	SLTPrint(plist);  //2->3->4->NULL
	SLTPopFront(&plist);
	SLTPrint(plist);  //3->4->NULL
}


int main()
{
	SlistTest03();
	return 0;
}

运行结果:
 

 

好,我们已经实现了头部和尾部的插入和删除的操作,接下来我们来实现一下查找的操作~

//查找
SLTNode* SLTFind(SLTNode** pphead, SLTDataType x)
{
	assert(pphead);

	//遍历链表
	SLTNode* pcur = *pphead;
	while (pcur) //等价于pcur != NULL
	{
		if (pcur->data == x) {
			return pcur;
		}
		pcur = pcur->next;
	}

	//没有找到
	return NULL;

}

接下来测试一下:
 

void SlistTest03()
{
	SLTNode* plist = NULL;
	SLTPushBack(&plist, 1);
	SLTPushBack(&plist, 2);
	SLTPushBack(&plist, 3);
	SLTPushBack(&plist, 4);

	SLTNode* FindRet = SLTFind(&plist,1); //1 在链表中,可以找到

	if (FindRet) {
		printf("找到了!\n");
	}
	else {
		printf("未找到!\n");
	}

}



int main()
{

	SlistTest03();

	
	return 0;
}

运行结果:

接下来我们看一下在指定位置插入数据~

它分为2种,在指定位置之前插入和在指定位置之后插入数据

先看在指定位置之前插入数据

它要分要插入的位置是头节点和不是头节点2种情况讨论哦

实现代码如下:
 

//在指定位置之前插入数据

void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{

	assert(pphead);
	assert(*pphead);//链表 不能为空!
	assert(pos);
	

	SLTNode* newnode = SLTBuyNode(x);

	//pos刚好是头结点
	if (pos == *pphead) 
	{
		//头插
		SLTPushFront(pphead, x);
		return;
	}

	//pos不是头结点的情况
	SLTNode* prev = *pphead;
	while (prev->next != pos)
	{
		prev = prev->next;
	}
	
	prev->next = newnode;
	newnode->next = pos;

}

好,我们来测试一下~

void SlistTest03()
{
	SLTNode* plist = NULL;
	SLTPushBack(&plist, 1);
	SLTPushBack(&plist, 2);
	SLTPushBack(&plist, 3);
	SLTPushBack(&plist, 4);

	SLTNode* FindRet = SLTFind(&plist,1);

	SLTInsert(&plist, FindRet, 100); 
	SLTPrint(plist);//预期是100->1->2->3->4->NULL


}

int main()
{

	SlistTest03();

	
	return 0;
}

运行结果:
 

接下来我们再看一下在指定位置之后插入数据SLTInsertAfter吧~

这个实现起来要比在指定位置之前插入要简单

我们看代码:
 

//在指定位置之后插入数据
void SLTInsertAfter(SLTNode* pos, SLTDataType x)
{

	assert(pos);
	SLTNode* newnode = SLTBuyNode(x);

	
	newnode->next = pos->next;  //特别注意一下这里的顺序哦~
	pos->next = newnode;


}

写完后也测试一下:
 


void SlistTest03()
{
	SLTNode* plist = NULL;
	SLTPushBack(&plist, 1);
	SLTPushBack(&plist, 2);
	SLTPushBack(&plist, 3);
	SLTPushBack(&plist, 4);

	SLTNode* FindRet = SLTFind(&plist, 1);

	SLTInsertAfter(FindRet, 100); 
	SLTPrint(plist);//预期是1->100->2->3->4->NULL


}

int main()
{

	SlistTest03();

	
	return 0;
}

测试一下:

那么,插入讲完了,我们接下来再看一下删除操作

分别是删除pos节点以及删除pos之后的节点

先看一下删除pos节点  的情况吧~

//删除pos节点
void SLTErase(SLTNode** pphead, SLTNode* pos)
{
	assert(pphead);
	assert(*pphead);
	assert(pos);

	//pos刚好是头结点,没有前驱节点,执行头删
	if (*pphead == pos) {
		//头删
		SLTPopFront(pphead);
		return;
	}

	//pos不是头结点
	SLTNode* prev = *pphead;
	while (prev->next != pos)
	{
		prev = prev->next;
	}
	
	prev->next = pos->next;
	free(pos);
	pos = NULL;


}

下面来测试一下:

void SlistTest03()
{
	SLTNode* plist = NULL;
	SLTPushBack(&plist, 1);
	SLTPushBack(&plist, 2);
	SLTPushBack(&plist, 3);
	SLTPushBack(&plist, 4);

	SLTNode* FindRet = SLTFind(&plist, 4);

	SLTErase(&plist, FindRet);
	SLTPrint(plist);//预期是1->2->3->NULL


}

int main()
{

	SlistTest03();

	
	return 0;
}


 

运行结果:

再看一下删除pos之后的节点吧~

下面是实现的代码~



//删除pos之后的节点
void SLTEraseAfter(SLTNode* pos)
{
	assert(pos);
	//pos->next不能为空
	assert(pos->next);

	SLTNode* del = pos->next;  //定义一个中间的变量用来保存
	pos->next = pos->next->next;
	free(del);
	del = NULL;



}

下面进行测试:
 

void SlistTest03()
{
	SLTNode* plist = NULL;
	SLTPushBack(&plist, 1);
	SLTPushBack(&plist, 2);
	SLTPushBack(&plist, 3);
	SLTPushBack(&plist, 4);

	SLTNode* FindRet = SLTFind(&plist, 2);

	SLTEraseAfter(FindRet);
	SLTPrint(plist);//预期是1->2->4->NULL


}

int main()
{

	SlistTest03();

	
	return 0;
}

好,最后我们来看一下链表的销毁操作吧~

//销毁链表
void SListDesTroy(SLTNode** pphead)
{

	assert(pphead);
	assert(*pphead);

	SLTNode* pcur = *pphead; //pur依旧是作为临时变量,用于保存~
    
   while (pcur)
    {
	SLTNode* next = pcur->next;
	free(pcur);
	pcur = next;
    }

		
	*pphead = NULL;

}

关于链表的销毁,我们可以通过调试来观察,这里就不再演示了,大家可以自己测试一下~

好,到这,我们就把单链表的实现给讲完了~(鼓掌鼓掌)

好,那么这里又出现了一个新的问题,我们在这里花了这么多精力说了单链表的各种操作,那么链表究竟有多少种类呢?它与单链表又是什么关系呢?

接下来,我们就来说说链表的分类

链表的分类

不知道大家有没有想过为什么我创建的这个工程名为SList?

其实它是Single Linked list 的简写,也就是单链表的意思

我们上面的对链表的各种插入,删除都是对单链表进行操作的

那其实 链表的种类有很多,单链表的全称就是不带头单向不循环链表

我们在平时为了方便就称为单链表了~

既然有不带头就有带头的,由单向也就有双向的,有不循环的也就有循环的

如此这般三三组合,其实就可以推出链表的种类有2*2*2=8种

各个种类的关系如图:

看到这么多种类的链表,大家也不要太焦虑,去想单单一种类型的单链表就学了这么久,更何况还有7种.....

其实,我们实际中最常用只有两种结构:单链表带头双向循环链表(简称双向链表),后者我们会在之后的博客中进行介绍与分享的~

最后我们在来看一下单链表双向链表各自的一些特点吧~
1.单链表(不带头单向不循环链表):结构简单,一般不会单独用来存数据。实际中更多是作为其他数据结构的子结构,如哈希桶、图的邻接表等等

这种结构也是在笔试面试中出现很多


2.双向链表(带头双向循环链表):结构最复杂,一般用在单独存储数据

实际中使用的链表数据结构,都是带头双向循环链表。另外这个结构虽然结构复杂,但是使用代码实现以后会发现结构会带来很多优势,所以实现反而简单了,这个我们代码实现了就知道了,这里只要先大致有一个印象就行,不必担心~

结语

好了,今天关于链表的分享就到这里了

在学习编程的道路上Humble与各位同行,加油吧各位!

最后希望大家点个免费的赞或者关注吧(感谢感谢),也欢迎大家订阅我的专栏

让我们在接下来的时间里一起成长,一起进步吧!

1d8bd2383fe54a7aa576bdd8d41dc462.png

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/344218.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Spring Boot 初始(快速搭建 Spring Boot 应用环境)

提示: ① 通过下面的简介可以快速的搭建一个可以运行的 Spring Boot 应用(估计也就2分钟吧),可以简单的了解运行的过程。 ② 建议还是有一点 Spring 和 SpringMVC的基础(其实搭建一个 Spring Boot 环境不需要也没有关系…

C++多线程_std::future与std::promise

目录 1. 引言 2. promise/future的含义 std::future std::promise std::packaged_task std::async 处理异常 std::shared_future 实战:多线程实现快速排序 时钟与限定等待时间 参考: 1. 引言 在并发编程中,我们通常会用到一组非阻塞的模型&a…

共享wifi项目到底能不能做?

如今,互联网已经渗透到我们生活的方方面面,人们对WiFi的需求越来越大,已经成为人们不可或缺的一部分。在这样的背景下,共享WiFi项目应运而生,作为近年来兴起的创业选择,成为了越来越多创业者追逐的热门项目…

Servlet 与 MVC

主要内容 Servlet 重点 MVC 重点 Filter 重点 章节目标 掌握 Servlet 的作用 掌握 Servlet 的生命周期 掌握 JSP 的本质 掌握 MVC 的设计思想 掌握 Filter 的作用及使用场景 第一节 Servlet 1. Servlet 概念 Servlet 是在服务器上运行的能够对客户端请求进行处理&a…

微信小程序(八)图片的设定

注释很详细&#xff0c;直接上代码 上一篇 新增内容&#xff1a; 1.图片的三种常见缩放形式 2.图片全屏预览 源码&#xff1a; testImg.wxml <!-- 默认状态&#xff0c;不保证缩放比&#xff0c;完全拉伸填满容器 --> <image class"pic" mode"scaleTo…

Backtrader 文档学习-Target Orders

Backtrader 文档学习-Target Orders 1. 概述 sizer不能决定操作是买还是卖&#xff0c;意味着需要一个新的概念&#xff0c;通过增加小智能层可以决定买卖&#xff0c;即通过持仓份额可以决定买卖操作。 这就是策略中order_target_xxx方法族的作用。受zipline的方法的启发&am…

商城系统中30分钟未付款自动取消订单怎么实现(简单几种方法)

实现以上功能 方法1&#xff1a;定时任务批量执行 写一个定时任务&#xff0c;每隔 30分钟执行一次&#xff0c;列出所有超出时间范围得订单id的列表 AsyncScheduled(cron "20 20 1 * * ?")public void cancelOrder(){log.info("【取消订单任务开始】"…

Excel导出警告:文件格式和拓展名不匹配

原因描述&#xff1a; Content-Type 原因&#xff1a;Content-Type&#xff0c;即内容类型&#xff0c;一般是指网页中存在的Content-Type&#xff0c;用于定义网络文件的类型和网页的编码&#xff0c;决定文件接收方将以什么形式、什么编码读取这个文件&#xff0c;这就是经常…

【算法专题】动态规划之路径问题

动态规划2.0 动态规划 - - - 路径问题1. 不同路径2. 不同路径Ⅱ3. 珠宝的最高价值4. 下降路径最小和5. 最小路径和6. 地下城游戏 动态规划 - - - 路径问题 1. 不同路径 题目链接 -> Leetcode -62.不同路径 Leetcode -62.不同路径 题目&#xff1a;一个机器人位于一个 m …

linux安装docker--更具官网教程

1.访问https://docs.docker.com/ 2.进入download 3输入cento 或者直接访问地址Install Docker Engine on CentOS | Docker Docs 4一步一步根据官网命令走 2安装 常见报错&#xff1a; yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.rep…

node.js旅游景点分享网站03796-计算机毕业设计项目选题推荐(附源码)

摘 要 随着社会的发展&#xff0c;社会的各行各业都在利用信息化时代的优势。计算机的优势和普及使得各种信息系统的开发成为必需。旅游景点分享网站设计&#xff0c;主要的模块包括查看后台首页、轮播图&#xff08;轮播图管理&#xff09;、网站公告管理&#xff08;网站公告…

幻兽帕鲁安装和开服教学

《幻兽帕鲁》游戏热度异常火爆&#xff0c;很多玩家想下载《幻兽帕鲁》和朋友玩&#xff0c;但不知道在哪里能够下载到&#xff0c;下面请看《幻兽帕鲁》下载安装教学&#xff0c;希望能够帮助大家。 幻兽帕鲁》目前仅在PC上的Steam平台发售&#xff0c;可以登录Steam搜索“幻…

Dify学习笔记-基础介绍(一)

1、简介 Dify AI是一款强大的LLMOps&#xff08;Language Model Operations&#xff09;平台&#xff0c;专为用户提供便捷的人工智能应用程序开发体验。 该平台支持GPT系列模型和其他模型&#xff0c;适用于各种团队&#xff0c;无论是用于内部还是外部的AI应用程序开发。 它…

【java问题解决】-word转pdf踩坑

问题情境&#xff1a; 项目中采用word转pdf&#xff0c;最开始使用的pdf相关的apache的pdfbox和itextpdf&#xff0c;后面发现对于有图片背景的word转pdf的情景&#xff0c;word中的背景图会直接占用位置&#xff0c;导致正文不会正确落在背景图上。 解决方案&#xff1a; 采…

力扣日记1.23-【回溯算法篇】17. 电话号码的字母组合

力扣日记&#xff1a;【回溯算法篇】17. 电话号码的字母组合 日期&#xff1a;2023.1.23 参考&#xff1a;代码随想录、力扣 17. 电话号码的字母组合 题目描述 难度&#xff1a;中等 给定一个仅包含数字 2-9 的字符串&#xff0c;返回所有它能表示的字母组合。答案可以按 任意…

flink-java使用介绍,flink,java,DataStream API,DataSet API,ETL,设置 jobname

1、环境准备 文档&#xff1a;https://nightlies.apache.org/flink/flink-docs-release-1.17/zh/ 仓库&#xff1a;https://github.com/apache/flink 下载&#xff1a;https://flink.apache.org/zh/downloads/ 下载指定版本&#xff1a;https://archive.apache.org/dist/flink…

洛谷C++简单练习day4

day4---进制转化---1.22 习题概述 题目描述 今天小明学会了进制转换&#xff0c;比如&#xff08;10101&#xff09;2 &#xff0c;那么它的十进制表示的式子就是 : 1*2^40*2^31*2^20*2^11*2^0&#xff0c; 那么请你编程实现&#xff0c;将一个M进制的数N转换成十进制表示…

VSCode Debug 参数设置说明

如果想在vscode中debug一个项目&#xff0c;比如python3 run.py --args 这个时候你需要着重关注几个参数&#xff0c;参数用两个双引号分开&#xff0c;不能有空格。 cwd :运行代码的基础目录env: 设置环境变量 PYTHONPATH&#xff1a; 设置项目用到的模块搜索路径&#xff…

开源模型应用落地-KnowLM模型小试-入门篇(一)

一、前言 你是否了解知识图谱&#xff1f;如果了解&#xff0c;你们的业务场景是否应用了知识图谱&#xff1f;实际上&#xff0c;知识图谱在各行各业都被广泛使用。例如&#xff0c;在搜索引擎优化方面&#xff0c;通过利用知识图谱&#xff0c;搜索引擎可以更好地理解网页内容…

【英文干货】【Word_Search】找单词游戏(第3天)

本期主题&#xff1a;Doors&#xff08;各式各样的门&#xff09; 本期单词&#xff1a; Automatic (Door) 自动门 Back (Door) 后门 Barn (Door) 谷仓的门 Battened (Door) 用木条加固的门 Fire (Door) 防火门 Front (Door) 前门 Garage (Door) 车库的门 Glazed (Door…