线性表
前言
线性表、顺序表、链表之间的区别和联系。
- 线性表:逻辑结构,不用管底层如何实现,只要求逻辑上是一个接着一个有先后的顺序即可。顺序表、链表都是一种线性表。
- 顺序表、链表:物理结构,是一种真实存在在物理内存上的线性结构,比如顺序表就是用数组实现。而链表主要用指针完成工作。不同的结构在不同的场景有不同的区别。
下面的图有助于清晰了解。
![]()
线性表基本架构
对于一个线性表来说,不管它的具体实现如何,它们的方法函数名和实现效果应该一致(即使用方法相同、达成逻辑上的效果相同,差别的是实现方式可能针对不同的场景效率不同)。
所以基于面向对象的编程思维,我们可以将线性表写成一个接口,而具体实现的顺序表和链表的类可以实现这个线性表的方法,以提高程序的可读性。还有一点非常重要,初学数据结构与算法时实现的线性表都是固定类型(例如int),随着知识的进步,我们应当采用泛型来实现更合理的方式。
顺序表
顺序表是一种线性表的存储结构,它通过一组连续的存储单元来存储数据元素。顺序表中的元素在内存中是连续存放的,可以通过元素在表中的位置(通常用索引来表示)来直接访问和操作元素。
顺序表的使用方法主要包括以下几个方面:
-
创建顺序表:首先需要确定顺序表的数据类型和最大容量,然后通过申请一块连续的内存空间来创建顺序表。
-
插入元素:向顺序表中插入元素时,可以在指定位置(索引)插入元素,也可以在表尾追加元素。在指定位置插入元素时,需要考虑元素的移动以及是否会导致表空间不足的情况。
-
删除元素:从顺序表中删除元素时,可以根据索引删除指定位置的元素,也可以删除表尾的元素。删除元素后,需要将后面的元素向前移动,填补被删除元素的空缺。
-
查找元素:可以根据元素的数值或者索引来查找顺序表中的元素,通常采用遍历或二分查找的方式进行查找操作。
-
修改元素:可以通过索引定位到指定位置的元素,然后对其数值进行修改。
-
获取长度:可以通过查询顺序表中元素的个数来获取顺序表的长度。
顺序表相比链表具有随机访问性能好的优点,但是在插入和删除元素时需要移动大量元素,性能较差。在实际应用中,需要根据具体的需求选择合适的数据结构来存储和操作数据。
代码实现
template <class T>
class SqlList{
private:
T * array;
int size;//表示当前顺序表中实际存储的元素数量
int capacity;表示当前顺序表内部数组的容量
public:
SeqList(int initCapicity = 10);//构造函数
~SeqList();//析构函数
//定义操作函数
void init(int initCapacity);
int length();
bool isEmpty();
int elemIndex(T value);//查找元素索引(位置)
T getElem(int index);//根据索引获取元素
void insert(int index,T value);
void remove(int index);
void set(int index,T value);//设置元素
void print();
private:
void resizeArray();
}
template <class T>
SeqList<T>::SeqList(int initCapicity)
{
init(initCapicity);
}
template <class T>
SeqList<T>:: ~SeqList() {
delete[] array;
}
template <class T>
void SeqList<T>::init(int initCapacity)
{
capacity = initCapacity;
array = new T[capacity];
size = 0;
}
template <class T>
int SeqList<T>::length() {
return size;
}
template <class T>
bool SeqList<T>::isEmpty() {
return size == 0;
}
template <class T>
int SeqList<T>::elemIndex(T value) {
for (int i = 0; i < size; i++) {
if (array[i] == value) {
return i;
}
}
return -1;
}
template <class T>
T SeqList<T>::getElem(int index){
if(index<0 || index >= size)
{
throw std::out_of_range("Index is out of bounds.");
}
return array[index];
}
template <class T>
void SeqList<T>::insert(int index, T value) {
if (index < 0 || index > size) {
throw std::out_of_range("Index is out of bounds.");
}
if (size == capacity) {
resizeArray();
}
for (int i = size; i > index; i--) {
array[i] = array[i - 1];
}
array[index] = value;
size++;
}
template <class T>
void SeqList<T>::remove(int index) {
if (index < 0 || index >= size) {
throw std::out_of_range("Index is out of bounds.");
}
for (int i = index; i < size - 1; i++) {
array[i] = array[i + 1];
}
size--;
}
template <class T>
void SeqList<T>::set(int index, T value) {
if (index < 0 || index >= size) {
throw std::out_of_range("Index is out of bounds.");
}
array[index] = value;
}
template <class T>
void SeqList<T>::resizeArray() {
capacity *= 2;
T* newArray = new T[capacity];
for (int i = 0; i < size; i++) {
newArray[i] = array[i];
}
delete[] array;
array = newArray;
}
链表
链表不同于顺序表,每个节点可能存在于不同的内存地址中,像一条链子一样,将这些不同地方的节点顺序连接在一起。指针指向(链表存储)了相邻节点的地址,节点能够通过这些指针找到下一个的节点形成一条链。
就物理存储结构而言,地址之间的联系是无法更改的,相邻地址就是相邻。但在链式存储中,下一个地址是由上一个节点"主动记录的",因此可以进行更改。
基本结构
对于线性表,我们只需要一个data数组和size就能表示基本信息。而对于链表,我们需要一个Node类节点(head头节点),和size分别表示存储的节点数据和链表长度,这个节点有数据域和指针域。数据域就是存放真实的数据,而指针域就是存放下一个Node类节点的指针,其具体结构为:
template <class T>
class Node{
public:
T data;
Node<T> * next;
Node(T data){
this->data = data;
this->next = NULL;
}
}
带头结点链表VS不带头结点链表
有许多人可能会对带头结点和不带头结点链表的区别感到困惑,甚至不清楚什么是带头结点和不带头结点。我来为大家阐述一下:
带头结点:在带头结点的链表中,head指针始终指向一个节点,这个节点不存储有效值,仅仅起到一个标识作用(有点像班主任带着学生)。
不带头结点:在不带头结点的链表中,head指针始终指向第一个有效节点,这个节点存储有效数值。
那么带头结点和不带头结点的链表有什么区别呢?
查找方面:在查找操作上,它们没有太大区别,带头结点需要多进行一次查找。
插入方面:对于非第0个位置的插入操作,区别不大,但不带头结点的链表在插入第0号位置之后需要重新改变head头指针的指向。

删除方面:对于非第0个位置的删除操作,区别不大,不带头结点的链表在删除第0号位置之后需要重新改变head头指针的指向。
- 头部删除(带头结点):在带头结点的链表中,头部删除操作和普通删除操作一样。只需执行
head.next = head.next.next,这样head的next直接指向第二个元素,从而删除了第一个元素。 - 头部删除(不带头结点):不带头结点的链表的第一个节点(head)存储有效数据。在不带头结点的链表中,删除也很简单,只需将head指向链表中的第二个节点即可,即:
head = head.next。
总而言之:带头结点通过一个固定的头可以使链表中任意一个节点都同等的插入、删除。而不带头结点的链表在插入、删除第0号位置时候需要特殊处理,最后还要改变head指向。两者区别就是插入删除首位(尤其插入),个人建议以后在使用链表时候尽量用带头结点的链表避免不必要的麻烦。
带头指针VS带尾指针
基本上是个链表都是要有头指针的,那么头尾指针是个啥呢?
头指针: 其实头指针就是链表中head节点,表示链表的头,称为为头指针。
**尾指针: **尾指针就是多一个tail节点的链表,尾指针的好处就是进行尾插入的时候可以直接插在尾指针的后面。

但是带尾指针的单链表如果删除尾的话效率不高,需要枚举整个链表找到tail前面的那个节点进行删除。
插入操作
add(int index,T value)
其中index为插入的编号位置,value为插入的数据,在带头结点的链表中插入那么操作流程为
- 找到对应index-1号节点成为pre。
node.next=pre.next,将插入节点后面先与链表对应部分联系起来。此时node.next和pre.next一致。pre.next=node将node节点插入到链表中。

当然,很多时候链表需要插入在尾部,如果频繁的插入在尾部每次枚举到尾部的话效率可能比较低,可能会借助一个尾指针去实现尾部插入。
代码实现
//不带头结点的单链表结构
#include<iostream>
template <class T>
class Node {
public:
T data;
Node<T>* next;
Node(T data) : data(data), next(nullptr) {}
};
template <class T>
class LinkedList{
private:
Node<T>* head;
public:
LinkedList() : head(nullptr) {}
void insertNode(T data) {
Node<T>* newNode = new Node<T>(data);
if (head == nullptr)
head = newNode;
else {
Node<T>* temp = head;
while (temp->next != nullptr)
temp = temp->next;
temp->next = newNode;
}
}
void deleteNode(T data) {//三种情况
if (head == nullptr)//为空
return;
if (head->data == data){//删除头结点
Node<T>* temp = head;
head = head->next;
delete temp;
return;
}
Node<T>* temp = head;//删除一般节点
while (temp->next != nullptr) {
if (temp->next->data == data) {
Node<T>* cur = temp->next;
temp->next = cur->next;
delete cur;
return;
}
temp = temp->next;
}
}
void printList() {
Node<T> * cur = head;
while (cur != nullptr){
std::cout << cur->data<<" ";
cur = cur->next;
}
std::cout << std::endl;
}
};
int main() {
LinkedList<int> list;
list.insertNode(1);
list.insertNode(2);
list.insertNode(3);
list.printList();
list.deleteNode(2);
list.printList();
return 0;
}
总结
这里的只是简单实现,实现基本方法。链表也只是单链表。完善程度还可以优化。
单链表查询速度较慢,因为他需要从头遍历,如果在尾部插入,可以考虑设计带尾指针的链表。而顺序表查询速度虽然快但是插入很费时,实际应用根据需求选择!


浙公网安备 33010602011771号