C++ 链表基本操作

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)
插入的基本思路:

  1. 判断位置是否合法
  2. 利用current遍历找到插入位置的前一个位置
  3. 创建新结点 node *newNode = new node( val );
  4. 把新结点的指针域指向current的下一个结点 : newNode->next = current->next;
  5. current的指针域指向newNode : current->next = newNode;
  6. 长度加一 (如果有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. 删除(通过位置)

删除的思路与插入类似,也是

  1. 先遍历到要删除结点前一个结点
  2. 利用一个temp指针暂存当前结点的下一个结点(要删除的结点)
  3. 把当前结点的指针域指向要删除的结点的下一个结点。必须先连接,否则会丢失后续结点
  4. 然后delete temp删除temp指向的结点。
  5. 长度减一(如果有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. 合并另一个链表

直接合并另一个链表的思路:

  1. 先判断B链表是否为空
  2. 找到A的末尾(如果有尾指针就可以跳过)
  3. 把A的尾结点的指针域指向B的第一个结点
  4. B置空
  5. 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可以直接判断)
思路如下:

  1. 创建链表类型
  2. 开始遍历两个链表
  3. 插入结点并且把该结点所在链表的指针后移
  4. 新链表的指针后移
  5. 重复3,4直至至少有一个链表被遍历完成
  6. 遍历插入长链表的剩余结点
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;

}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值