|
????????一个月没有更新了。上次讲了单链表的基础操作并且还简单介绍了一下指针的概念。简单回顾一下,单链表的两个结构体,一个是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;
}
}
}
}
}
????????谢谢浏览!编程小白会继续努力给大家分享的!!!
|