【学习记录】单向循环链表 20200402

421 阅读7分钟

循环链表

循环链表是另一种形式的链式存储结构。它的特点是表中最后一个结点的指针域指向首元结点,整个链表形成一个环

循环链表的结点定义可以参看我的上一篇博客。
或者你也可以参看本文所涉及的代码,希望能够帮助你理解循环链表。

创建以及初始化

循环链表在初始化或者添加数据初始化时需要注意:

  • 循环链表一般不需要添加额外的头结点,即首元结点就是链表的头。
  • 在添加新的结点在尾部时需要处理链表的尾结点,将其指向链表的首元结点

具体过程如下图:

循环链表在进行操作的时候一般最主要辅助操作就是寻找链表的尾结点

  • 遍历链表结点,若当前结点的下一个结点是首元结点,那么当前结点即为尾结点
  • 在连续操作的时候,每次操作的都记录尾结点便于下一次操作使用。 代码如下:
/*

/*
4.1 循环链表创建!
2种情况:① 第一次开始创建; ②已经创建,往里面新增数据

1. 判断是否第一次创建链表
   YES->创建一个新结点,并使得新结点的next 指向自身; (*L)->next = (*L);
   NO-> 找链表尾结点,将尾结点的next = 新结点. 新结点的next = (*L);
*/
Status createCycleChain(List *l){
    int itemValue;
    List temp = NULL;
//    遍历用来找尾
    List key = NULL;
//    每一次操作用来记录尾
//    List tail = NULL;
    printf("输入节点的值,输入0结束:\n");
    while (1) {
        scanf("%d", &itemValue);
        if (itemValue == 0) break;
        

        if (*l == NULL) {
//        如果链表为空,创建新的节点作为新链表的起始节点,next 要指向自己
            *l = (List)malloc(sizeof(Node));
            
            if (!l) exit(ERROR);
            (*l)->data = itemValue;
            (*l)->next = *l;
//            tail = *l;
        }else{
//            如果链表不为空,需要找到链表的尾,将新的数据插入作为新的尾,并使其next指向头。
//            找尾有两种方法:
//            1.遍历链表,若某节点的next指向头,那么它就是尾 2.每一次操作的时候将尾记录下来。
            for (key = *l; key->next != *l; key = key->next) ;
            
//            创建新的节点
            temp = (List)malloc(sizeof(Node));
            temp->data = itemValue;
            temp->next = *l;
            key->next = temp;
            
//            temp->data = itemValue;
//            temp->next = *l;
//            tail->next = temp;
//            tail = temp;
        }
    }
    return SUCCESS;
}

输出

循环链表的打印输出很简单,主要涉及到链表尾结点的判断。


//4.2 遍历循环链表,循环链表的遍历最好用do while语句,因为头节点就有值

/// 遍历循环链表
/// @param l 链表
Status printCycleChain(List l){
    
    if (!l) {
        return ERROR;
    }else{
        List t;
        t = l;
        printf("链表信息\n");
        do {
            printf("%d, ", t->data);
            t = t->next;
        } while (t != l); // 若当前节点为头,说明已经打印完毕
        printf("\n");
    }
    return SUCCESS;
}

插入结点

对于插入,由于循环链表没有添加头结点,所以对于循环链表的插入需要讨论插入位置是不是在链表的首元位置

插入位置在首元位置

步骤:

  • 创建新的结点
  • 找寻链表的尾结点
  • 新的结点指向首元结点
  • 尾结点指向新的结点;
  • 使新的结点成为链表的头

整体过程如图所示:

插入位置不在首元位置

步骤:

  • 创建新的结点
  • 找寻插入位置的前置结点
  • 新的结点指向前置结点的下一个结点
  • 前置结点指向新的结点。 整体过程如图所示:

综合两种情况,代码如下:


/// 在指定位置为链表插入新的结点
/// @param l 链表
/// @param index 位置
/// @param value 新的值
Status chainInsert(List *l, int index, int value){
    
    List temp;
    
    temp = (List)malloc(sizeof(Node));
    if (!temp) {
        return ERROR;
    }
    temp->data = value;
    
    //    如果插入位置为首元结点,需要特殊处理:
    //    1.创建新的结点 temp, 新结点指向原来的头 ;
    //    2.原来的尾结点指向新的结点;
    //    3.将新结点变为新的头。
    if (index == 1) {
        List tail;
        for (tail = *l; tail->next != *l; tail = tail->next) ;
        
        temp->next = *l;
        tail->next = temp;
        *l = temp;
    }else{
    //    如果插入位置不是首元:(注意如果超出位置,就放在最后一个结点上)
    //    1.创建新的结点 temp, 找到插入位置的前置结点,新的结点 指向 前置结点的下一个结点;
    //    2.前置结点指向新节点;
        int i;
        List front; // 前置结点
        for(i = 1, front = *l; front->next != *l && i != index - 1; front = front->next, i++) ;
        
        if (i != index - 1) {
             printf("没找到正确位置!\n");
            return ERROR;
        }
        
        temp->next = front->next;
        front->next = temp;
        
    }
    return SUCCESS;
}

删除结点

和插入结点类似,因为循环链表没有添加头结点,所以对于循环链表的删除需要讨论删除位置是不是在链表的首元位置。

删除位置在首元位置

步骤:

  • 暂记下首元结点,它就是待删除结点
  • 找寻链表的尾结点
  • 尾结点指向待删除结点的下一个结点
  • 将待删除结点的下一个结点作为链表的头;
  • 带回待删除结点的值并释放该节点。 整体过程如图所示:

建设中...

删除位置不在首元位置

步骤:

  • 找寻插入位置的前置结点
  • 暂存前置结点的下一个结点,它就是待删除结点
  • 前置结点指向待删除结点的下一个结点
  • 带回待删除结点的值并释放该节点。 整体过程如图所示:

建设中...

综合两种情况,代码如下:


/// 删除链表特定位置的结点
/// @param l 链表
/// @param index 删除位置
/// @param value 带回删除结点的值
Status chainDelete(List *l, int index, int *value){
    List temp;
    
    if (!l) {
        return ERROR;
    }
    
//    如果删除的是首元,需要做特殊操作:
//    1.使用temp指针标记首元结点,找寻尾结点;
//    2.尾结点指向temp的下一个结点;
//    3.使temp的下一个结点成为链表的头;
//    4.带回temp的值,释放temp.
    if (index == 1) {
        temp = *l;
        
        // 若链表只有一个元素了,那就 删除,释放头结点,将链表置空。
        if (temp->next == *l) {
            *value = temp->data;
            free(temp);
            *l = NULL;
            return SUCCESS;
        }
        
        List tail;
        
        for (tail = *l; tail->next != *l; tail = tail->next) ;
    
        tail->next = temp->next;
        *l = temp->next;
        *value = temp->data;
        free(temp);
        
    }else{
//        若删除的不是链表的首元结点:
//        1.找到删除结点的前置结点 front,使用 temp 标记 front 的下一个结点,它就是需要删除的结点;
//        2.使前置结指向temp的下一个结点;
//        3.删除并释放temp结点。
        List front;
        int i;
        
        for (i = 1, front = *l; front->next != *l && i != index - 1; front = front->next, i++) ;
        
        if (i != index - 1) {
            printf("没找到正确位置!\n");
            return ERROR;
        }
        
        temp = front->next;
        front->next = temp->next;
        *value = temp->data;
        free(temp);
        
    }
    return SUCCESS;
}

查询

遍历链表寻找特定位置,如果找到带回值即可。注意尾结点的判断,还有对于结束循环还有可能没有找到的判断。


/// 查找链表特定位置结点的值
/// @param l 链表
/// @param index 位置
/// @param value 带回找到的值
Status chainGet(List l, int index, int *value){
    if (!l) {
        return ERROR;
    }
    List temp;
    int i;
    for (temp = l,i = 1; i < index && temp->next != l; temp = temp->next, i++) ;
    
    if (temp == l) {
//        根本就没有进入循环,只赋值了初始条件,确认是不是就是找的第一个元素
        if (index == 1) {
            *value = temp->data;
            return SUCCESS;
        }
    }else{
        if (temp->next == l && i != index) {
//           循环结束了而且找到了最后一个元素,确认是不是要找的元素
            return ERROR;
        }else{
//           在中间就结束了循环,那一定是找到了
            
//        带回记录值
           *value = temp->data;
           return SUCCESS;
        }

    }
    return ERROR;
}

建设中...(其实没有实现的必要了,你要是上面都懂了自然就会了哈哈,再不济,删除 + 插入 = 改 😊)


其实,对于循环链表首元结点的特殊处理本人有一些个人看法:

无论是删除还是插入,链表操作时最主要的行为就是要找到操作位置的前置结点,只不过对于循环链表来说,首元结点的前置结点就是尾结点而已;然后进行插入或者删除操作;操作完毕之后,如果操作位置是首元结点,那么需要将链表的头替换成新的头即可。

可以发现整个思路和线性链表是一致的,从思想上可以完全不用考虑是不是循环链表😊