赞
踩
链表是由一系列连接在一起的结点构成,其中的每个结点都是一个数据结构。
链表的结点通常是动态分配、使用和删除的,允许链表在程序运行时增大或缩小。如果需要将新信息添加到链表中,则程序只需分配另一个结点并将其插入到系列中。如果需要从链表中删除特定的信息块,则程序将删除包含该信息的结点。
链表的每个节点,除数据之外还包含一个后继指针指向链表中的下一个节点。
非空链表的第一个结点称为链表的头。要访问链表中的结点,需要有一个指向链表头的指针。从链表头开始,可以按照存储在每个结点中的后继指针访问链表中的其余结点。最后一个结点中的后继指针被设置为 nullptr 以指示链表的结束。
因为指向链表头的指针用于定位链表的头部,所以也可以认为它代表了链表头。同样的指针也可以用来定位整个链表,从头开始,后面跟着后续指针,所以也可以很自然地把它看作是代表了整个链表。
下图给出了一个由 3 个结点组成的链表,其中显示了指向头部的指针,链表的 3 个结点以及表示链表末尾的 nullptr 指针。
头节点:可有可无
有时,在链表的第一个节点之前会额外增设一个节点,该节点的数据域一般不存放数据(有些情况下也可以存放链表的长度等信息),此节点被称为头节点。
若链表中存在头节点,且头节点的指针域为空(NULL),表明链表是空表。
头节点对于链表来说,不是必须的,换句话说,一个完整的链表中可以不设有头节点。那么,可能有人会问:既然头节点无关紧要,那它有什么作用?在处理某些问题时,给链表添加头节点会使问题变得简单。
首元节点
链表中第一个元素所在的节点,它是头节点后边的第一个节点。
其实,首元节点和链表中存放数据的其他节点没什么不同,只是因为该节点位于链表的头部,所以被称为首元节点。
头指针
链表的头指针永远指向链表中第一个节点的位置,换句话说,如果链表有头节点,头指针指向头节点;否则,头指针指向首元节点。
一个链表可以头节点,但不能没有头指针。
头节点和头指针的区别是:
- 头指针是一个指针,头指针指向链表的头节点或者首元节点;
- 头节点是一个实际存在的节点,它包含有数据域和指针域。
头节点和头指针的区别在程序中的直接体现是:头指针只声明而没有分配存储空间,头节点需要声明并分配一个节点的实际物理内存。
在实际中,链表有许多形式,根据单向、双向;带头、不带头;循环、非循环的排列组合,就有八种形式的链表。
下面举几个常见的链表:
- 无头单向非循环链表:结构简单,一般不会单独用来存数据。实际中更多是作为其他数据结构的子结构,如哈希桶、图的邻接表等等。另外这种结构在笔试面试中出现很多。
- 带头双向循环链表:结构最复杂,一股用在单独存储数据。 实际中使用的链表数据结构,都是带头双向循环链表。另外这个结构虽然结构复杂,但是使用代码实现以后会发现结构会带来很多优势,实现反而简单了,后面我们代码实现了就知道了。
两者的区别:
数组的优点
数组的缺点
3. 插入和删除的效率低,需要移动其他元素。
4. 会造成内存的浪费,因为内存是连续的,所以在申请数组的时候就必须规定七内存的大小,如果不合适,就会造成内存的浪费。
5. 内存空间要求高,创建一个数组,必须要有足够的连续内存空间。
6. 数组的大小是固定的,在创建数组的时候就已经规定好,不能动态拓展。
链表的优点
7. 插入和删除的效率高,只需要改变指针的指向就可以进行插入和删除。
8. 内存利用率高,不会浪费内存,可以使用内存中细小的不连续的空间,只有在需要的时候才去创建空间。大小不固定,拓展很灵活。
链表的缺点
查找的效率低,因为链表是从第一个节点向后遍历查找。
struct ListNode
{
double value;
ListNode *next;
//构造函数
ListNode(double valuel, ListNode *nextl = nullptr)
{
value = value1;
next = next1;
}
};
通过该声明,即可使用以下两种不同的方式创建一个结点:
- 通过仅指定其 value 部分,而后继指针则默认为 nullptr。
- 通过指定 value 部分和一个指向链表下一个结点的指针。
例如,使用numberList作为链表头,使用 numberFile 作为输入文件对象,则以下代码将读取存储在某个文本文件中的数字,并将它们排列在链表中:
ListNode *numberList = nullptr;
double number;
while (numberFile >> number)
{
//创建一个结点以保存该数字
numberList = new ListNode(number, numberList);
}
从链表头开始,涉及整个链表,并在每个结点上执行一些处理操作的过程被称为遍历链表。
例如,如果需要打印某个链表中每个结点的内容,则必须遍历该链表。假设某个链表的链表头指针是 numberList,要遍历该链表,则需要使用另一个指针 ptr 指向链表的开头:
ListNode *ptr = numberList;
然后就可以通过使用表达式 *ptr 或者使用结构指针操作符 -> 来处理由 ptr 指向的结点。例如,如果需要打印在结点上的值,则可以编写以下代码:
cout << ptr->value;
一旦在该结点的处理完成,即可将指针移动到下一个结点(如果有的话),其语句如下:
ptr = ptr->next;
以上语句使用指向结点后继的指针来替换了指向该结点的指针,实现了结点之间的移动。因此,要打印整个链表,可以使用如下代码:
ListNode *ptr = numberList;
while (ptr != nullptr)
{
cout << ptr->value << " "; //处理结点(显示结点内容)
ptr = ptr->next; //移动到下一个结点
}
下面的程序演示了上面所介绍的各种技巧,即读取文件中的数字,将数字排列在链表中,然后通过遍历链表将数字显示在屏幕上。
// This program illustrates the building // and traversal of a linked list. #include <iostream> #include <fstream> using namespace std; struct ListNode { double value; ListNode *next; // Constructor ListNode(double value1, ListNode *next1 = nullptr) { value = value1; next = next1; } }; int main() { double number; // Used to read the file ListNode *numberList = nullptr; // List of numbers // Open the file ifstream numberFile("numberFile•dat"); if (!numberFile) { cout << "Error in opening the file of numbers."; exit (1); } //Read the file into a linked list cout << "The contents of the file are: " << endl; while (numberFile >> number) { cout << number << " "; // Create a node to hold this number numberList = new ListNode(number, numberList); } // Traverse the list while printing cout << endl << "The contents of the list are: " << endl; ListNode *ptr = numberList; while (ptr != nullptr) { cout << ptr->value << " "; // Process node ptr = ptr->next; // Move to next node } return 0; }
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。