最近在学习线性表中的链表,正好预习复习一下。
main函数在文末。
直接上代码
不停地翻炒知识,才能收获真正的味道。
虽然现在还是有很多问题,但是在慢慢解决了。
1 2 3 4 5 6
| typedef struct LNode { int key; struct LNode* next; }LNode;
|
创建链表
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| LNode* Create_a_list() { LNode* head = (LNode*)malloc(sizeof(LNode)); if (head == NULL) { return NULL; } //如果head内存分配失败的话,返回NULL。 //提高代码的健壮性和安全性。
LNode* tail = head; //tail = (LNode*)malloc(sizeof(LNode)); tail->next = NULL; int value;
while (scanf("%d", &value)==1)//如果输入的是非数值字符将会停止循环 { if (value <= 0) break; LNode* p = (LNode*)malloc(sizeof(LNode)); p->key = value; p->next = NULL;//最新一个节点的next一定为NULL tail->next = p;//将新节点与链表最后一个节点相连 tail = p;//将指示指针移动到新节点处 }
return head->next;//返回的是有效的第一个节点 }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| LNode* Create_a_list() { LNode* head = (LNode*)malloc(sizeof(LNode)); if (head == NULL) { return NULL;//对head是否成功分配内存进行判断。 } head->next = NULL; int n, i; scanf("%d", &n); for (i = 0; i < n; i++) { int value; scanf("%d", &value); LNode* p = (LNode*)malloc(sizeof(LNode)); p->key = value; p->next = head->next; head->next = p; } return head->next; }
|
💡Tips
- 当一个结构体指针需要存东西的时候,就要开辟内存空间。如果它只是复制其他的指针,则不需要开辟空间。
- 此处代码返回的是 LNode* 类型,所以要返回指针。在创建链表中,就要返回头节点指针。此处的 head->next 指向的是有效的第一个节点。
- 安全性检查。
打印链表
1 2 3 4 5 6 7 8 9 10 11
| void Print_a_list(LNode* L) { LNode* p; p = L; while (p->next != NULL) { printf("%d->", p->key); p = p->next; } printf("%d\n", p->key); }
|
删除链表
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| void Delete_a_list(LNode* L) { LNode* p = L; while (p != NULL) { p = L->next; free(L); L = p; } if (L == NULL) { printf("It's done.\n"); } }
|
💡Tips
- 一般是用两个指针并行移动,一个负责记录和保存,另一个负责删改和变换。此处就是p和L。
返回链表长度
1 2 3 4 5 6 7 8 9 10 11
| int Get_length(LNode* L) { LNode* head = L; int i = 0; while (head != NULL) { i++; head = head->next; } return i; }
|
💡Tips
- 虽然很简单,但仍要强调一下,避免犯一些蠢钝如猪的错误。🐷🐷🐷
这里用头节点自身移动没什么太大问题,但是在链表的元素有改动的时候,记得另外定义一个节点复制头节点的值来保存头节点指针。如果直接移动头节点指针,那么整个链表就乱套了。😖😖😖
返回链表节点值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| int Get_value(LNode* L, int i) { LNode* head = L; int j = 0;
/* for (; j < i-1; j++) { head = head->next; } */
//改进 while (head != NULL && j < i - 1) { head = head->next; j++; } if (head == NULL) { return 0; } printf("%d\n", head->key); return head->key; }
|
💡Tips
- 返回类型据结构体具体内容而定,以及其中的一些细节输出也要视具体的结构体而定。
- 依旧是注意安全性检查。
插入元素
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| LNode* List_insert(LNode* L, int value, int location) { LNode* head = L; LNode* p = (LNode*)malloc(sizeof(LNode)); p->key = value;
if (location==1) { p->next = head; head = p; return head; }
int i = 1; while (location > i && head->next!=NULL) { head = head->next; i++; }
p->next = head->next; head->next = p; return head; }
|
- 注意返回的是指针类型。因为如果头节点被修改,那么该链表的头节点指针需要更新。
但是,返回类型也可以是void。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| void List_insert(LNode** L, int value, int location) { LNode* head = *L;//注意这里指针是指向头节点的指针 LNode* p = (LNode*)malloc(sizeof(LNode)); p->key = value; if (location == 1) { p->next = head; head = p; *L = head;//只需要在location为1的时候更新头节点即可 return; } int i = 1; while (i < location && head->next != NULL) { head = head->next; i++; } p->next = head->next; head->next = p; }
|
💡Tips
- 因为你要修改指针,所以要传指针的指针。即你要修改什么,就传那个东西的指针。
- 传指针和传值的区别:
🔍传指针:实际上传的是指针指向的某个变量的地址,那么你在函数体内修改指针,就是在修改这个地址储存的内容,是从根本上修改。
🔍传值:在函数传参中,传值传参只是创建了该变量的副本,在函数体内的修改也只是对副本进行修改,并没有对本体造成实质性的改变。
删除节点
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| LNode* List_delete(LNode* L, int location) { LNode* head = L,*p = L;
int i = 1; while (i < location-1 && p != NULL) { p = p->next; i++; } LNode* p1 = p->next; p->next = p1->next; free(p1); return head; }
|
返回指定位置的元素
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| LNode* Get_location(LNode* L, int value) { LNode* p = L; while (p != NULL && p->key != value) { p = p->next; } if (p == NULL) { printf("该节点不存在\n"); } return p; }
|
删除指定值的第一个节点
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| void Delete_pointed_key(LNode** L, int value) { LNode* p = *L; LNode* q=NULL; while (p->next != NULL && p->key != value) { q = p; p = p->next; } if (q == NULL) { *L = p->next; } else { q->next = p->next; } free(p); }
|
删除指定值的所有节点
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| void Delete_pointed_key_all(LNode** L, int value) { LNode* p = *L; LNode* q = NULL; while (p != NULL) { //检查头节点key是否等于value if (p->key == value) { LNode* tmp = p; p = p->next; if (q == NULL)//说明此时是头指针 { *L = p; } else { q->next = p; free(tmp); } free(tmp); //将头节点指针移到下一个节点 } else { q = p; p = p->next; } } }
|
💡Tips
- (一些心理话)链表的代码其实就是在描述链表移动增删查改的过程。后续如果有机会,做一个链表的动画,这样搭配代码食用更佳。
合并有序数组
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| LNode* Merge_linked_list(LNode* L1, LNode* L2) { LNode* p = L1, * q = L2; LNode* r=(LNode*)malloc(sizeof(LNode));//创建虚拟头节点 r->next = NULL; while(p != NULL && q != NULL) { if (p->key < q->key) { r->next = p; p = p->next; } else if (p->key > q->key) { r->next = q; q = q->next; } else { r->next = p; LNode* tmp = q; p = p->next; q = q->next; free(tmp); } r = r -> next; } if (p == NULL) { r->next = q; } else { r->next = p; } return L1; }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| LNode* Merge_linked_list(LNode* L1, LNode* L2) { LNode* p = L1, * q = L2, *r = L1; while(p != NULL && q != NULL) { if (p->key < q->key) { r = p; p = p->next; } else if (p->key > q->key) { r = q; q = q->next; } else { r = p; LNode* tmp = q; p = p->next; q = q->next; free(tmp); } r = r -> next; } if (p == NULL) { r->next = q; } else { r->next = p; } return L1; }
|
💡Tips
原版的问题主要出于逻辑上的漏洞。
- 原版想实现的逻辑是将两个链表合并到其中一个链表中。
- r=p 或者 r=q都是想将当前节点赋给链表r中,但因为后续有 r=r->next ,上述赋值会导致r向p或者q的链表移动,不太准确。
- 改进版同样实现了这个逻辑,但语法更健壮。
- 先初始化r为虚拟头节点,注意此时r后续会连接节点,即要存储内容,所以r要初始化且开辟内存空间。
- 将原版改成了 r->next=p 以及 r->next=q ,就不会出现r沿p或者q的链表移动的情况。因为r始终在两个节点的前一个节点,用 r->next 的话只需要置身“链”外选择下一个节点,而不会让自身处在两个链表里干涉选择。
main函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| int main() {
LNode* list = Create_a_list(); Print_a_list(list); int length=Get_length(list); printf("%d\n",length); Get_value(list, 3); Delete_a_list(list); LNode* list2 = Create_a_list(); list2=List_insert(list2, 7, 1); list_insert(&list2,7,1); Print_a_list(list2); list2=List_delete(list2, 3); Print_a_list(list2); Delete_pointed_key(&list2, 7); Print_a_list(list2); Delete_pointed_key_all(&list2, 3); Print_a_list(list2); list=Merge_linked_list(list, list2); Print_a_list(list); return 0; }
|
💡Tips
- 返回类型不为空时,要有相同类型的对象“去接住这个函数的返回结果”。e.g. list2=List_insert(list2, 7, 1);中的list2。
啰哩巴嗦的总结
以下几点是链表中最常出错的地方:
- 创建链表时是否保留了虚拟头节点。因为本文创建链表返回的是有效而非虚拟头节点指针,所以在链表的其他操作中有时会预先定义虚拟头节点。(一通写下来似乎有虚拟头节点更方便🤔)
- 如果创建的指针有内容要存储(例如该指针会插入链表),那么一定要初始化分配内存空间。如果只是复制其他头节点指针,则不需要开辟空间。
- 特别注意不要直接移动链表原初的头节点,先复制保存头结点指针值,再进行操作(例如删除节点)。直接移动指针值的话就很容易意识不到头节点的改变。
- 用两个指针并行移动,一个负责记录和保存,另一个负责删改和变换。
- 在对链表元素有增删时,有两种方式,一种是传头节点指针的指针,返回类型为空;一种是传头节点指针,返回类型为指针(返回改动后链表的头节点指针)。(详见插入元素。)
- 安全性检查十分重要。当遍历链表时很容易超出链表范围,用上判断条件 p!=NULL 就会有保障得多。
- 哦对了,还有一个小点,传指针的指针时,要记得取指针的地址!!!
心得
心得就是没有心得。
感觉自己在有些方面还是考虑不周,或者思路十分怪异导致代码实现起来很拧巴。
还有就是链表的范围取定,很容易报错呢哈哈哈。
Anyway,温故常新。
要想学得好,多做多思考!(向刘红salute🫡)