线性表,是数据结构中非常基础的一种数据结构,分顺序表和链式表两种;
顺序表就是我们常用的数组,链式表就是通过指针来串联元素;
数组与链表的区别:
1. 内存分配方式:
- 数组:连续的内存块用于存储元素。在创建数组时,需要一次性分配足够大小的连续内存空间。
- 链表:通过指针将离散的节点连接在一起存储元素。每个节点都可以在内存中的任何位置,它们使用指针来指向下一个节点。
2. 访问操作:
- 数组:由于连续存储,对于已知索引的元素访问非常高效。通过索引计算偏移量,可以在O(1)时间内直接访问元素。
- 链表:在访问元素时需要从头节点开始遍历整个链表,直到找到目标元素。平均情况下,链表的元素访问时间复杂度为O(n),其中n是链表长度。
3. 插入和删除操作:
- 数组:插入和删除操作的效率相对较低。插入或删除元素后,需要移动其他元素以保持连续性,这可能需要较长的时间,尤其是在大型数组中。
- 链表:插入和删除操作的效率相对较高。在链表中插入或删除一个节点只需要修改指针,不需要移动其他节点。
4. 动态大小调整:
- 数组:数组的大小在创建时就确定了,无法动态地改变大小。如果需要更大的数组,必须重新分配内存并复制现有元素。
- 链表:链表可以动态地增长或缩小。通过修改指针连接,可以在运行时轻松地添加或删除节点。
5. 存储空间开销:
- 数组:由于需要连续的内存块,往往会浪费一部分内存空间,特别是当数组大小不确定或需求变化时。
- 链表:由于使用离散的节点,不需要额外的存储空间来处理动态性,因此可以更有效地利用内存,但是指针也是占空间的。
链表的定义:
// 单链表
struct ListNode {
int val; // 节点上存储的元素
ListNode *next; // 指向下一个节点的指针
};
链表的实现(附带详细注释):
//单链表
typedef struct Node {
int data;
struct Node *next;
} linkNode, *linkList;
//创建链表
linkList createList(int *arr, int size) {
linkList L = (linkList) malloc(sizeof(linkNode)); //创建头结点
L->next = NULL; //初始化头结点指针为空
linkList p = L; //创建一个指针指向头结点
for (int i = 0; i < size; ++i) { //遍历数组,创建新结点并插入链表尾部
p->next = (linkList) malloc(sizeof(linkNode));
p = p->next;
p->data = arr[i]; //将数组元素赋值给新结点的数据域
p->next = NULL; //将新结点的指针初始化为空
}
return L; //返回链表的头结点
}
//输出链表中的数据
void outputData(linkList L) {
while (L->next) {
L = L->next;
printf("%d\t", L->data);
}
}
//释放链表的内存空间
void freeLinklist(linkList L) {
while (L->next) { //遍历链表,释放每个结点的内存空间
linkList p = L->next;
L->next = L->next->next;
free(p);
}
free(L); //释放头结点的内存空间
}
//指定位置插入节点
void insertNode(linkList L, int position, int value) {
int flag = 0;
linkList p = L;
while (p && flag < position) { //遍历链表,找到插入位置的前一个结点
p = p->next;
++flag;
}
if (p && flag == position) { //如果找到位置,则插入新结点
linkList newNode = (linkList) malloc(sizeof(linkNode));
newNode->data = value; //将新结点的数据域赋值为要插入的值
newNode->next = p->next; //将新结点的指针指向原来下一个结点
p->next = newNode; //将前一个结点的指针指向新插入的结点
} else {
printf("无效位置\n");
}
}
//删除指定位置的结点
void deleteNode(linkList L, int position) {
int flag = 0;
linkList p = L;
while (p->next && flag < position) { //遍历链表,找到要删除的位置的前一个结点
p = p->next;
++flag;
}
if (p->next && flag == position) { //如果找到位置,则删除结点
linkList delNode = p->next; //定义一个指针指向要删除的结点
p->next = delNode->next; //将前一个结点的指针指向要删除结点的下一个结点
free(delNode); //释放要删除结点的内存空间
} else {
printf("无效位置\n");
}
}
//删除链表中值为指定值的结点
int deleteNumber(linkList L, int target) {
linkList p = L;
int count = 0;
while (p->next) {
if (p->next->data == target) { //如果找到指定数的结点,则删除
linkList delNum = p->next;
p->next = delNum->next;
free(delNum);
++count;
continue; //继续下一次循环,防止删除多个连续的相同数的结点
}
p = p->next;
}
return count;
}
//修改指定位置的结点值
void modifyNode(linkList L, int position, int newNum) {
int flag = 0;
linkList p = L;
while (p && flag < position) { //遍历链表,找到指定位置的结点
p = p->next;
++flag;
}
if (p && flag == position) { //如果找到位置,则修改结点的值
p->data = newNum; //修改结点的数据域为新的值
} else {
printf("无效位置\n");
}
}
//查找指定值的结点位置
int selectPos(linkList L, int flagNum) {
int pos = 0;
linkList p = L->next;
while (p) {
if (p->data == flagNum) {
printf("值的下标位置是:%d\n", pos);
return pos;
}
p = p->next;
++pos;
}
return -1;
}
//查询倒数第K个结点的值(双指针法)
void seekBack(linkList L, int pos) {
if (L == NULL || pos <= 0) {
return;
}
linkList slow = L;
linkList fast = L;
// 快指针先走word-1个节点
for (int i = 0; i < pos; ++i) {
fast = fast->next;
if (fast == NULL) { //结点数小于word,退出
return;
}
}
//同时移动快指针和慢指针,直到快指针到达链表末尾
while (fast) {
slow = slow->next;
fast = fast->next;
}
printf("%d\n", slow->data);
//若是删除,则新建一个新节点保存待删节点,再free掉即可
//linklist p = NULL;
//p = slow->next;
//slow->next = slow->next->next;
//free(p);
}
//翻转链表
void linkListReversal(linkList L, int size) {
linkList temp;
linkList front = L;
linkList later = NULL;
while (front) { //意味着这个节点下面还有节点,还没有翻转完成
temp = front->next;// 保存一下 front的下一个节点,因为接下来要改变front->next
front->next = later;// 翻转操作
later = front;// 更新 front和 later指针
front = temp;
}
//return later;若用函数,返回值是later
for (int i = 0; i < size; i++) {
printf("%d ", later->data);
later = later->next;
}
}
//合并有序链表
linkList mergeLinklist(linkList L1, linkList L2) {
linkList L3 = (linkList)malloc(sizeof(linkNode)); // 创建新链表头节点
linkList p = L3; // 用于遍历新链表
linkList p1 = L1->next; // 遍历链表L1
linkList p2 = L2->next; // 遍历链表L2
while (p1 && p2) {
if (p1->data <= p2->data) {
p->next = p1;
p1 = p1->next;
} else {
p->next = p2;
p2 = p2->next;
}
p = p->next;
}
// 将未遍历完的链表剩余部分连接到新链表尾部
p->next = p1 ? p1 : p2;
return L3;
}
//去除链表中的重复项
void deDuplicate(linkList L) {
linkList fast = L;
linkList slow = NULL;
if(L == NULL){
return;
}
while (fast->next != NULL) {
if (fast->data == fast->next->data) {
slow = fast->next;
fast->next = fast->next->next;
free(slow);
continue;
//不可或缺,执行continue,跳出当前循环,就可以正确删除第二第三重复节点
//考虑以下情况:
//假设链表中有三个连续的重复节点,以及其他不重复的节点。
// 在删除第一个重复节点之后,由于没有continue语句,会继续执行循环中的下一条语句(即'fast = fast->next;'),
// 即移动fast指针到下一个节点。这样一来,会直接跳过第二个和第三个重复节点,无法删除它们。
}
fast = fast->next;
}
}
本文为C语言基础知识的应用,要求熟练理解指针、结构体的定义,并正确的使用;
可以看出,C语言相较于C++,缺少了函数的封装性以及代码的重复利用性,之后会更新关于C++的基础知识,以及C++引以为傲的STL库的介绍相关的实现;
本文为C语言基础知识分享,没有一个程序员的代码是完美的,本人水平有限,如有错误,请补充和指正!