1. 链表价值
链表实现了内存零碎数据的有效组织。比如,当用malloc来进行申请内存时,当内存足够,但由于碎片过多,没有连续内存,只能失败,此时就要用链表来组织数据解决了
2. 静态链表
静态链表不可以动态扩容
2.1. 链表节点定义
typedef struct node
{
int data; // 数据域,要存储的数据
struct node *next; // 指针域,指针指向下一个节点,下一个节点的类型是什么样子的,就是本类型,指向就是存储了谁的地址
} Node;
void traverseList(Node *head)
{
// 遍历逻辑
// 当前指针:指向第一个元素,并且不为空
// 打印第一个元素的值,同时能得到第二个元素的地址
// 第二个元素的地址,赋值给当前指针
while (head)
{
printf("%d", head->data);
head = head->next;
}
}
int main()
{
Node a,b,c,d,e,f; // 此节点均在栈上
Node *head;
head = &a; // 1 开头
a.data = 1; // 2 内容
b.data = 2;
c.data = 3;
d.data = 4;
e.data = 5;
a.next = &b; // 3 链接
b.next = &c;
c.next = &d;
d.next = &e;
e.next = NULL; // 4 结尾
traverseList(head);
return 0;
}
3. 动态链表
静态链表的意义不是很大,主要原因是数据存储在栈上,栈的存储空间有限,不能动态分配。要实现存储自由,要动态的申请堆里的空间。
3.1. 链表名字解释
3.1.1. 空链表
3.1.2. 非空链表
3.2. 创建
3.2.1. 尾插法
尾插法:在尾节点插入,每插入一个节点,即成尾节点
// 定义节点结构体,包含数据域和指向下一个节点的指针
typedef struct node
{
int data;
struct node *next;
} Node, *NodePtr; // NodePtr 是 Node 指针类型的别名。
// 遍历链表并打印每个节点的数据
void traverseList(Node *head)
{
head = head->next; // 跳过头结点,指向第一个有效元素
while (head)
{
printf("%d\n", head->data);
head = head->next; // 指向下一个节点
}
}
Node *createList()
{
Node *head = (Node *)malloc(sizeof(Node));
if (NULL == head)
exit(-1);
head->next = NULL;
Node *t = head, *cur; // t指针指向头结点,作为尾指针
int nodeData;
scanf("%d", &nodeData);
// 当输入的数据不为0时,继续创建新节点
while (nodeData)
{
// 为新节点分配内存空间
cur = (Node *)malloc(sizeof(Node)); // 申请空间
// 如果分配失败,退出程序
if (NULL == head)
exit(-1);
// 将数据放入新节点
cur->data = nodeData;
// 尾指针的next指向新节点
t->next = cur;
// 尾指针更新为当前新节点
t = cur;
scanf("%d", &nodeData);
}
// 最后一个节点的next指针指向NULL
t->next = NULL;
return head;
}
int main()
{
Node *head = createList();
traverseList(head);
return 0;
}
3.2.2. 头插法
头插法:在头节点后面插入元素,每插入一个元素,即为首节
Node *createList()
{
Node *head = (Node *)malloc(sizeof(Node)); // 创建头结点
if (NULL == head)
exit(-1);
head->next = NULL; // 初始化头结点的next指针
Node *cur; // 当前节点指针
int nodeData; // 节点数据
scanf("%d", &nodeData); // 输入第一个节点数据
while (nodeData) // 在栈循环的时候,要注意一重循环和二重循环的一致性
{
cur = (Node *)malloc(sizeof(Node)); // 为新节点分配内存
if (NULL == cur)
exit(-1);
cur->data = nodeData; // 将数据放入新节点
cur->next = head->next; // 新节点的next指针指向当前头结点的next指针
head->next = cur; // 头结点的next指针指向新节点
scanf("%d", &nodeData); // 输入下一个节点数据
}
return head; // 返回头结点
}
3.3. 求长度
int lenList(Node *head)
{
int len = 0;
head = head->next;
while (head)
{
len++;
head = head->next;
}
return len;
}
3.4. 链表的增删改查
加入头节点的目的,是对于链表的所有操作,不需要更新头指针
3.4.1. 插入操作
带头节点的链表,插入元素时,即 使新插入的元素成为首元素,不需要更新头节点指针
头插法思想,更适合插入操作,如果采用尾插法思想,则效率较低,比头插法每次都多一次遍历
因此,创建链表的操作可以改写成:创建空链表 * 插入,实际生产中是这样的
// 真正意义上的创建链表,就是创建一个空链表
Node *createList()
{
Node *head = (Node *)malloc(sizeof(Node));
if (NULL == head)
exit(-1);
head->next = NULL; // 初始化头节点的 next 指针为 NULL
return head; // 返回头节点指针
}
// 插入操作,本质就是头插法
void insertList(Node *head, int nodeData)
{
Node *cur = (Node *)malloc(sizeof(Node)); // 为新节点分配内存
if (NULL == cur)
exit(-1);
cur->data = nodeData; // 设置新节点的数据
cur->next = head->next; // 将新节点的 next 指针指向当前头节点的 next 指针
head->next = cur; // 将头节点的 next 指针指向新节点
}
int main()
{
Node *head = createList();
for (int i = 0; i < 10; i++)
{
insertList(head, i);
}
traverseList(head);
return 0;
}
3.4.2. 查找操作
// 查找
Node *searchList(Node *head, int findData)
{
head = head->next;
while (head)
{
if (head->data == findData)
break;
head = head->next;
}
return head;
}
int main()
{
Node *head = createList();
for (int i = 0; i < 10; i++)
{
insertList(head, i);
}
traverseList(head);
printf("len of list = %d\n", lenList(head));
Node *pfind = searchList(head, 5);
if (pfind == NULL)
printf("find failed\n");
else
printf("found %d\n", pfind->data);
return 0;
}
3.4.3. 删除和修改操作
带头节点的链表删除元素时,既是删除链表的首元素,也是删除头节点的最后一个元素,不需要更新表头指针
void *deleteNodeOfList(Node *head, Node *pfind)
{
while (head->next != pfind) // head就是pfind的前驱
head = head->next;
head->next = pfind->next;
free(pfind);
pfind = NULL;
}
int main()
{
Node *head = createList();
for (int i = 0; i < 10; i++)
{
insertList(head, i);
}
traverseList(head);
printf("len of list = %d\n", lenList(head));
Node *pfind = searchList(head, 0);
if (pfind == NULL)
printf("find failed\n");
else
{
pfind->data = 100; // 改
printf("found %d\n", pfind->data);
deleteNodeOfList(head, pfind);
}
traverseList(head);
return 0;
}
3.5. 排序
3.5.1. 冒泡排序
#define N 5
int main()
{
int arr[N] = {5, 4, 3, 2, 1};
for (int i = 0; i < N - 1; i++)
{
for (int j = 0; j < N - i - 1; j++)
{
if (arr[j] > arr[j + 1])
{
arr[j] ^= arr[j + 1];
arr[j + 1] ^= arr[j];
arr[j] ^= arr[j + 1];
}
}
}
for (int i = 0; i < N; i++)
{
printf("%d\n", arr[i]);
}
return 0;
}
3.5.2. 换值排序
void popSortList(Node *head)
{
int len *lenList(head);
head = head->next;
Node *p, *q;
for (int i = 0; i < len - 1; i++)
{
p = head; // 每次内重循环从头开始
q = p->next; // q总是指向p的下一个节点,也就是被比较的节点
for (int j = 0; j < len - 1 - i; j++)
{
if (p->data > q->data)
{
p->data ^= q->data;
q->data ^= p->data;
p->data ^= q->data;
}
p = p->next;
q = p->next;
}
}
}
3.5.3. 换指针排序
交换节点数据域的方式,当节点数据域很大时,效率低下, 用换指针的方式提高效率
void popSortList(Node *head)
{
int len = lenList(head);
Node *prep, *p, *q, *t;
for (int i = 0; i < len - 1; i++)
{
prep = head;
p = head->next;
q = p->next;
for (int j = 0; j < len - 1 - i; j++)
{
if (p->data > q->data)
{
prep->next = q;
p->next = q->next;
q->next = p;
t = p;
p = q;
q = t;
}
prep = prep->next;
p = p->next;
q = p->next;
}
}
}
3.6. 链表反转
void reverseList(Node *head)
{
Node *cur = head->next;
head->next = NULL;
Node *t;
while (cur)
{
t = cur;
cur = cur->next;
t->next = head->next;
head->next = t;
}
}
3.7. 链表销毁
// 有多少malloc就应该有多少free
void destroyList(Node *head)
{
Node *t;
while (head)
{
t = head;
head = head->next;
free(t)
}
}