绝对好理解的双链表建立以及常用操作(插入,删除,翻转等)讲解

        一个月没有更新了。上次讲了单链表的基础操作并且还简单介绍了一下指针的概念。简单回顾一下,单链表的两个结构体,一个是node,我们类比为房屋,里面储存房屋的各种信息,例如人口等,另一个结构体是list,我们类比为街道,里面储存了街道上第一个房屋和最后一个房屋的位置,即头尾指针,而街道内的每一个房屋都有一个路牌,它指向下一个房屋的位置,这就是next指针。

        那么接下来就是双链表的讲解,双链表就是在单链表next指针的基础上增加了一个prev指针,作用就是用来指向上一个node,有了双链表,c语言的用法会灵活许多,在后续讲解图论时大家会有明显的感受。

        首先给出双链表的两个核心结构体:

typedef struct node {
    struct node *prev; //前指针
    struct node *next; //后指针
    char *data;
} Node;

typedef struct dll {
    Node *tail; //头指针
    Node *head; //尾指针
} Dll;

        从本质上来讲,单双链表的意义上相同的,所以如果单链表能理解,双链表就特别容易理解 了,我用一个手画草图来演示:

         这样就非常直观了是不是?

        那么废话不多说,我们直接开始讲解在使用双链表时常用的几大操作。注意,具体操作细节都在注释内。

基础部分:

1. 街道创建(list的创建)

Dll *DLL_create() {
    Dll *new_list = malloc(sizeof(Dll));
    new_list->head = new_list->tail = NULL; //一定要记得初始化
    return new_list;
} 

2.房屋创建(node的创建)

//如果要在链表内储存字符串类型的信息,就必须用复制的方法传入信息
char *copyString(char *value) { 
    char *data = malloc(sizeof(char) * (strlen(value) + 1));
    strcpy(data, value);
    return data;
}


Node *create_new_house(char *value) {
    Node *new_house = malloc(sizeof(Node));
    new_house->next = NULL;
    new_house->prev = NULL; //同样不要忘记这里的初始化(类似于单列表中设置每个新的房子单指针是NULL)
    //再次强调,因为data是字符串,不能用直接传入的方法,而应该用复制的方法传入数据,故才有了以下再次使用malloc的过程
    new_house->data = copyString(value); //复制字符串
    return new_house;
}

3.插入新的node至链表的头/尾/中间

        首先也是我先用手画图帮大家理解一下,然后我们再看代码。

        首先无论是插入到哪里,我们都要先通过刚刚写出来的create_new_house函数创建一个全新的初始化后的node,然后:

        这是插入到双链表末端的示意图:

        大体思路就是先用新的node的prev指针指向链表的tail,在用tail的next指针指向这个新的node,最后吧tail的位置挪到这个新的node的位置即可。

         插入到开头也是同理:

        先用新的node的next指针指向链表的head,在用head的prev指针指向这个新的node,最后吧head的位置挪到这个新的node的位置即可。 

        接下来是代码展示:

//末尾插入链表
Dll *DLL_append(Dll *list, char *value) {
    if (list != NULL) {
        //需要check链表的头、尾和中间
        if (list->head == NULL) { //这是指目前链表内还没有任何node
            Node *new_house = create_new_house(value);
            new_house->data[strlen(value)] = '\0'; //以防不同电脑的系统差异,应该在字符串最后强制加一个空字符串
            list->head = new_house; 
            list->tail = new_house;
        } else {
            Node *need_insert_house = create_new_house(value);
            //重点!!!
            need_insert_house->prev = list->tail;
            list->tail->next = need_insert_house;
            list->tail = need_insert_house;
        }
    }
    return list;
}


//开头插入链表
Dll *DLL_prepend(Dll *list, char *value) {
    if (list != NULL) {
        if (list->head == NULL) {
            list->head = list->tail = create_new_house(value);
        } else {
            Node *new_house = create_new_house(value);
            new_house->next = list->head;
            list->head->prev = new_house;
            list->head = new_house;
        }
    }
    return list;
}
//寻找到目标字符串后在其前面插入新的链表,如果找不到目标就插入到最开头(类似于插到中间,大家可以自行简化)
void DLL_insertBefore(Dll *list, char *target, char *value) {
    Node *target_list = is_exist(list, target); //这里是判断目标node是否存在,进阶部分会讲解这个函数
    if (target_list == NULL) {
        DLL_prepend(list, value); //未找到,插入到最后
    } else {
        Node *new_list = create_new_house(value);
        if (target_list == list->head) { //要额外考虑目标是头指针的情况
            list->head = new_list;
            new_list->prev = target_list->prev;
            target_list->prev = new_list;
            new_list->next = target_list;
        } else { //这里就是目标node在中间的情况了(理解不了就根据代码画图!!!)
            target_list->prev->next = new_list; //注意,这四个的顺序不能错!
            new_list->next = target_list;
            new_list->prev = target_list->prev;
            target_list->prev = new_list;
        }
    }
}

4.删除头/尾的node

        如果已经掌握了插入的方法,那么删除就特别好理解了。但是这里有一个特殊情况要注意,就是一定要多加一个判断来判断链表内是否只有一个node,如果只有一个node,在free掉这个node后一定一定一定要记得重新初始化链表的头尾指针为NULL,不然后续的程序会报错。

        上代码(我这里的程序是删除并且返回被删除的内容,大家可以根据需求自行修改):

//显示最后元素并删除
char *DLL_shrink(Dll *list) {
    char *result = NULL;
    if (list != NULL && list->head != NULL) {
        //判断是否只有一个的方法是判断head是否等于tail
        if (list->head == list->tail) {
            Node *delete_house = list->tail;
            result = copyString(delete_house->data);
            list->head = list->tail = NULL;//千万记得加不然会变成野指针
            free(delete_house);
            return result;
        } else {
            Node *delete_house = list->tail;
            delete_house->prev->next = delete_house->next; //意思是把tail的上一位的next设置为NULL
            list->tail = delete_house->prev; //把tail往前挪一位
            result = delete_house->data;
            free(delete_house);
            return result;
        }
    } else {
        return NULL;
    }
}

//显示开头元素并删除
char *DLLshift(Dll *list) {
    char *result = NULL;
    if (list != NULL && list->head != NULL) {
        //判断是否只有一个的方法是判断head是否等于tail
        if (list->head == list->tail) {
            Node *delete_house = list->tail;
            result = copyString(delete_house->data);
            list->head = list->tail = NULL;//记得初始化不然会报错,而且很难排查!
            free(delete_house);
            return result;
        } else {
            Node *delete_house = list->head;
            delete_house->next->prev = delete_house->prev; //意思是把原第一位设置为NULL
            list->head = delete_house->next; //head往后挪一位
            result = delete_house->data;
            free(delete_house);
            return result;
        }
    } else {
        return NULL;
    }
}

进阶部分 :

1.复制链表

        中心思想是创建一个新的list,然后找到被复制链表的头指针,在循环下不断调用末尾插入函数插入被复制链表的node内容,完成复制。

Dll *DLL_copy(Dll *list) {
    Dll *new_list = NULL;
    if (list != NULL) {
        new_list = DLL_create();
        Node *find_last = list->head;
        while (find_last != NULL) {
            DLL_append(new_list, find_last->data);
            find_last = find_last->next;
        }
    }
    return new_list;
}

2.翻转链表

        把链表内容整个翻转。在写的时候一定要注意分析node个数的奇偶。主要思路是tail指针不停指向下一个,prev不停指向上一个,在奇数个node时,head和tail会相遇;如果是偶数个那么head->next ==  tail 和 tail->prev == head这两种情况会相等,相遇就是函数的循环终止条件,在终止前不停交换头尾数据即可。

Dll *DLL_reverse(Dll *list) {
    if (list != NULL && list->head != NULL) {
        Node *head = list->head;
        Node *tail = list->tail;
        while (true) { 
            //针对奇数
            if (head == tail) {
                break;
            }
            char *exchange = head->data;
            head->data = tail->data;
            tail->data = exchange;
            //针对偶数
            if (head->next == tail) {
                break;
            }
            head = head->next;
            tail = tail->prev;
        }
    }
    return list;
}

3.判断链表是否有按顺序排列

        这里以字符串举例所以要用到strcmp函数。以下三个函数特别易懂,就不多做解释了。直接看代码吧。    

bool DLLisOrdered(Dll *list) {
    bool result = true;
    if (list != NULL && list->head != NULL) {
        Node *first_element = list->head;
        while (first_element != NULL && first_element->next != NULL) {
            if (strcmp(first_element->data, first_element->next->data) > 0{
                return false;
            }
            first_element = first_element->next;
        }
    }
    return result;
}

4.用于判断目标node是否存在

Node *is_exist(Dll *list, char *target) {
    Node *final_list = NULL;
    if (list != NULL && list->head != NULL) {
        Node *find_target = list->head;
        while (find_target != NULL) {
            if (strcmp(find_target->data, target) == 0) {
                final_list = find_target;
                break;
            }
            find_target = find_target->next;
        }
    }
    return final_list;
}

5.找到目标字符串后将其内容替换

void DLL_replace(Dll *list, char *target, char *value) {
    if (list != NULL && list->head != NULL) {
        Node *find_target = list->head;
        while (find_target != NULL) {
            if (strcmp(find_target->data, target) == 0) {
                find_target->data = copyString(value);
                break;
            }
            find_target = find_target->next;
        }
    }
}

高级部分:

        如果到这里都看懂了,其实链表也就掌握的差不多了,当然这只是C语言的,C++会更复杂大家可以查阅其他资料。

        剩下还有两个链表方法想和大家分享,其实也不是特别高级,只是略微比前面复杂一点点。

1.链表内容排序

        排序的各种方法相信大家都耳熟能详了,这里选用的是冒泡排序。

Dll *DLL_sort(Dll *list) {
    //这个是记录node数量的函数,太简单就不单独写了
    size_t length = DLL_length(list); 
    Node *pre_list = list->head; //使用快慢指针
    Node *cur_list = pre_list->next; 
    //以下为冒泡排序的算法内容,这个就不多做解释了
    for (int i = 0; i < length - 1; i++) {
        for (int j = 0; j < length - i - 1; j++) {
            if (strcmp(cur_list->data, pre_list->data) < 0) {
                char *exchange = pre_list->data;
                pre_list->data = cur_list->data;
                cur_list->data = exchange;
            }
            if (cur_list->next != NULL)
                cur_list = cur_list->next;
        }
        if (pre_list->next != NULL) { //这一步的目的是为了归位
            pre_list = pre_list->next;
            cur_list = pre_list->next;
        }
    }
    return list;
}

2.有序的不重复的插入链表内容

        这个是对之前的链表插入函数的改良,用这个函数时当你插入node时会一边插入一边自动排序。这个函数在图论中会用到。同时在图论里我还会补充插入子链表的知识点,有兴趣的可以关注一下我更新图论以及后续字符串匹配的知识点!

void list_append_by_order(list l, string item) {
        //这个contain就相当于之前的exist函数,用于避免重复的情况
    if (l != NULL && list_contains(l, item) == false){
        if (l->head == NULL) {
            l->head = l->tail = create_node(item);
            l->length++;
        } else if (strcmp(item, l->head->data) < 0) { //比第一个数小的情况
            list_push(l, item);
            l->length++;
        } else if (strcmp(item, l->tail->data) > 0) { //比最后一个数大的情况
            Node newNode = create_node(item);
            newNode->prev = l->tail;
            l->tail->next = newNode;
            l->tail = newNode;
            l->length++;
        } else { //在中间,但这里不考虑有重复元素
            Node temp = l->head;
            while (temp != NULL && temp->next != NULL) {
                if (strcmp(item, temp->data) > 0 && strcmp(item, temp->next->data) < 0) {
                    Node newNode = create_node(item);
                    newNode->next = temp->next;
                    newNode->prev = temp;
                    temp->next->prev = newNode;
                    temp->next = newNode;
                    l->length++;
                    break;
                }
            }
        }
    }
}

        谢谢浏览!编程小白会继续努力给大家分享的!!!

  • 5
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值