C++ 链表的基本操作实现
1. 前置
环境:
- Ubuntu22.04
- gcc 11.4
- C++20
2. 基本定义
把链表和结点分开定义,可以在链表中添加额外的成员变量来标识一些信息,方便后续的操作,例如表示长度的 int length
或者是可以添加尾指针 node *tail
struct node
{
//结点结构体
node *next;
int val; //假设为int
node( int val ) : val( val ), next( nullptr ){};
node( ):node(0){};//委托构造函数
};
链表的定义可以分为两种: 带头结点和带头指针。带头指针的链表是指针直接指向第一个元素,而带头结点是利用一个结点的指针域来指向第一个元素,头结点的数据域可以是任何数,但是如果是有序列表,则头结点最好使用数据类型的最小值。
头结点相对于头指针有一个显著的优势是代码对齐。在后续函数的初始化中,头结点的代码时一致的,不需要判断是不是第一个结点。因此本文将会使用头结点的链表
struct linkList
{
node head; //带头结点的
int length;
// node *tail;//尾指针,可以用来判断是否遍历到结尾
linkList() : head( node( INTMAX_MIN ) ), length( 0 ) {}
//INT_MAX是在<limits>的中的一个宏
// 一些函数声明
node *insert( int pos, int val );
void remove( int pos );
node* remove( int val );//函数重载
void clear();
node *find( int val );
void merge( linkList *B );
static linkList *merge( linkList *A, linkList *B );
// 静态成员函数,使用linkList::merge来调用
};
3. 插入函数
插入函数有一个值得注意的点,插入的位置可以正好等于链表的长度,相当于在链表的结尾插入一个结点(push_back)
插入的基本思路:
- 判断位置是否合法
- 利用
current
遍历找到插入位置的前一个位置 - 创建新结点
node *newNode = new node( val );
- 把新结点的指针域指向
current
的下一个结点 :newNode->next = current->next;
- 把
curren
t的指针域指向newNode
:current->next = newNode;
- 长度加一 (如果有length)
注意,第4和第5不能交换位置,如果交换,则会丢失current以后的所有结点
node *linkList::insert( int pos, int val )
{
if ( pos < 0 || pos > length )
//判断合法
return nullptr;
node *current = head.next;
for ( int i = 0; i < pos; ++i )
{
//遍历找到前一个结点
current = current->next;
}
node *newNode = new node( val );
newNode->next = current->next;
current->next = newNode;
length++;
return newNode;
}
4. 删除(通过位置)
删除的思路与插入类似,也是
- 先遍历到要删除结点前一个结点
- 利用一个
temp
指针暂存当前结点的下一个结点(要删除的结点) - 把当前结点的指针域指向要删除的结点的下一个结点。必须先连接,否则会丢失后续结点
- 然后
delete temp
删除temp指向的结点。 - 长度减一(如果有length)
void linkList::remove( int pos )
{
if ( pos < 0 || pos >= length )
return;
node *current = head.next;
for ( int i = 0; i < pos; ++i )
{
current = current->next;
}
node *temp = current->next;
current->next = temp->next;
delete temp;
length--;
}
5. 删除(通过值)
该函数实现删除第一个值为val的结点,然后返回删除结点的下一个结点,这是为了方便连续调用
node* linkList::remove( int val )
{
node *current = head.next;
while ( current->next != nullptr )
{
if ( current->next->val == val )
{
node *temp = current->next;
current->next = temp->next;
delete temp;
length--;
return current;
}
current = current->next;
}
return nullptr;
}
连续调用实例: 删除所有值为val的结点
node *p=nullptr;
int val=3;
while( (p= AlinkList.remove(val))!=null){}
假设AlinkList中有多个val,那么在删除第一个后,p会指向已经被删除的结点的下一个结点,此时p不为空,则会进行查找删除,直至没有val结点为止
6. 清除
清除并且释放所有结点,直接把指针置空会操作内存泄露
void linkList::clear()
{
node *current = head.next;
node *temp = nullptr;
while ( current != nullptr )
{
temp = current;
current = current->next;
delete temp;
}
head.next = nullptr;
length = 0;
}
7. 查找
查找思路比较简单,通过一个指针循环比较即可,注意代码中先比较,如果当前结点不是要查找的结点,在移动指针
node *linkList::find( int val )
{
node *current = head.next;
while ( current != nullptr )
{
if ( current->val == val )
{
return current;
}
current = current->next;
}
return nullptr;
}
8. 合并另一个链表
直接合并另一个链表的思路:
- 先判断B链表是否为空
- 找到A的末尾(如果有尾指针就可以跳过)
- 把A的尾结点的指针域指向B的第一个结点
- B置空
- A长度增加
void linkList::merge( linkList *B )
{
if ( B == nullptr )
return;
node *current = this->head.next;
while ( current->next != nullptr )
{
current = current->next;
}
current->next = B->head.next;
length += B->length;
B->head.next = nullptr;
B->length = 0;
}
这种合并类似移动语义,实际上并没有进行复制,而是进行了资源所有权转移,尤其是在指针的
vector<int> v2=std::move(v1);
9. 合并两个链表
与上一个合并的不同,该静态成员函数是在堆区开辟新的空间,合并两个链表并且返回这个新链表。
但是,由于两个链表的长度可能不一致,因此第一个循环同时遍历两个链表,按照从小到大的顺序来插入结点,插入结点所在的链表的指针后裔。当有一个链表被变量完成后,需要第二次循环来遍历插入另一个链表的剩余结点。由于实际调用时不知道是哪个链表短,因此需要两个循环来确保两个链表都被完全合并。(如果linkList有成员变量length可以直接判断)
思路如下:
- 创建链表类型
- 开始遍历两个链表
- 插入结点并且把该结点所在链表的指针后移
- 新链表的指针后移
- 重复3,4直至至少有一个链表被遍历完成
- 遍历插入长链表的剩余结点
static linkList *merge( linkList *A, linkList *B )
{
linkList *newList = new linkList();
node *current = newList->head.next ;
node *currentA = A->head.next;
node *currentB = B->head.next;
while ( currentA != nullptr && currentB != nullptr )
{
if ( currentA->val < currentB->val )
{
current->next = new node( currentA->val );
currentA = currentA->next;
}
else
{
current->next = new node( currentB->val );
currentB = currentB->next;
}
current = current->next;
newList->length++;
}
while ( currentA != nullptr )
{
current->next = new node( currentA->val );
currentA = currentA->next;
current = current->next;
newList->length++;
}
while ( currentB != nullptr )
{
current->next = new node( currentB->val );
currentB = currentB->next;
current = current->next;
newList->length++;
}
return newList;
}