杨老师b站视频如下,作为数据结构初学者收益很大,值得推荐:2 数据结构导论_哔哩哔哩_bilibili
.h文件 类型定义
typedef int ElemType;
//链表结点
typedef struct ListNode {
ElemType data;
struct ListNode* next;
}ListNode,*LinkList;
//随机链表结点
typedef struct RandomListNode {
ElemType data;
struct RandomListNode* next;
struct RandomListNode* random;
}RandomListNode;
.h文件 函数声明(与函数定义保持一致即可,可跳过)
ListNode* InitList();
ListNode* Buynode();
void PrintList(LinkList head);
void PrintNoHeadList(LinkList head);
void Add(LinkList head,int num);
void ReverseList(LinkList head);
void ReverseList2(LinkList head);
void ReverseList3(LinkList head);
void ReverseList4(LinkList head);
ListNode* Reverse(ListNode* p);
void Print_rev_K(LinkList head,int k);
void Print_rev_K2(LinkList head, int k);
ListNode* returnMid(LinkList head);
void IsRound(LinkList head);
void IsIntersection(LinkList headA, LinkList headB);
void IsIntersection2(LinkList headA, LinkList headB);
void RotateK(LinkList head,int k );
ListNode* Partition(LinkList head,int x);
void Quick_Sort(LinkList head);
void Quick_Sort_Fun(ListNode* begin, ListNode* end);
void Swap(int* pa, int* pb);
RandomListNode* Random_Copy(RandomListNode * head);
bool IsPalindrome(LinkList head);
bool IsPalindrome2(LinkList head);
因为本人写代码的时候是提取算法特征写的,所以一些是带头结点的,一些是不带头结点的,然后算法可能与杨老师讲的有一些差异,但是思路两者是一样的
所以我仅提供一个思路参考,也建议大家都自己提取算法特征(比如用了头插法)然后自己编写,效果会好很多。同时我只写了一些我觉得思路好的算法,还有一些有用的函数,一些基础的函数可能没有写。
.cpp文件 函数定义
引入头文件
#include<stdlib.h>
#include<stdio.h>
#include<string.h>
#include<assert.h>
#include "My_LinkList.h"
初始化链表
ListNode * InitList() {
//LinkList和 ListNode *类型应该来说能兼容
return Buynode();
}
ListNode* Buynode() {
LinkList s = (ListNode*)malloc(sizeof(ListNode)); //开辟空间
if (NULL == s) { exit(1); } //空间不足退出程序
memset(s, 0, sizeof(ListNode));//内容初始化为0
return s;
}
打印带头结点的链表
void PrintList(LinkList head) {
assert(head != NULL); //head为空跳过
ListNode* p = head->next;
while (p != NULL) {
printf("%d ", p->data);
p = p->next;
}
printf("\n");
}
打印不带头结点的链表
void PrintNoHeadList(LinkList head) {
assert(head != NULL);
ListNode* p = head;
while (p != NULL) {
printf("%d ", p->data);
p = p->next;
}
printf("\n");
}
其实两者的差别就是ListNode* p = head;与ListNode* p = head->next;
链表随机添加指定个数的元素
void Add(LinkList head,int num) { //head是链表地址 num是添加数量
assert(head != NULL);
for (int i = 0; i < num; i++) {
ListNode* s = Buynode();
s->next = head->next;
s->data = rand() % 100;
head->next = s;
}
}
单链表逆置算法1:逐个指向法(三指针法)
void ReverseList(LinkList head) {
assert(head != NULL);
ListNode* p = head->next;
ListNode* s = NULL, * pre = NULL;
while (p != NULL) {
s = p;
p = p->next;
s->next = pre;
pre = s;
}
head->next = s;//头结点指向最后一个
}
我对这个算法的理解就是,把链表所有的遍历一遍,依次指向前一个结点,s的作用是记录当前操作结点,pre是前面的结点,s->next = pre就是把后一个结点指向前一个结点,而p的作用是s->next改变你指向后会丢失s的下一个结点,所以p提前进入s的下一个结点,在改变s->next之后,s可以直接等于p
上面是老师的实现方法,我更偏向于下面这个写法,可能会更容易懂(思路一样)
void ReverseList(LinkList head) {
assert(head != NULL);
ListNode* after;
ListNode* s = head->next, * pre = NULL;
while ( s != NULL) {
after = s->next;
s->next = pre;
pre = s;
s = after;
}
head->next = pre;//头结点指向最后一个
}
这样的after就给一种即用即走的感觉,s->next改变之前 先把s->next放到after 改完再赋值到s上。
单链表逆置算法2:头插法
void ReverseList2(LinkList head) {
assert(head != NULL);
ListNode* p = head->next;
ListNode* after = NULL;//初始化一个after指针
//头插法的头结点要为空
head->next = NULL;
while (p != NULL) {
after = p->next;
p->next = head->next;
head->next = p;
p = after;
}
}
老师的代码是用已有函数头插,是取值头插,上面的代码是直接把结点链接过去,效率更快。也是有一个after指针,因为要改变p->next 所以after先存储p->next之后再赋值给p。
单链表逆置算法3:利用栈
void ReverseList3(LinkList head) {
assert(head != NULL);
int n = 0; //计算长度
ListNode * p = head->next;
while (p != NULL) {
n++;
p = p->next;
}
int* stack = (int*)malloc(sizeof(int) * n);
//稳健性处理
if (NULL == stack) {
exit(1); //如果空间不足就终值程序
}
p = head->next;
int top = -1; //栈的top指向
while (p != NULL) {
stack[++top] = p ->data; //这里的++top等于是先让top+1后再运算
p = p->next;
}
p = head->next;//再次遍历要把p指针重新赋值成head->next
//反向遍历赋值
while (p != NULL) {
p->data = stack[top--];
p = p->next;
}
//稳健性处理 处理malloc
free(stack);
stack = NULL;
}
单链表逆置算法4:递归算法
void ReverseList4(LinkList head){
ListNode* p = Reverse(head->next);//逆置函数
//p是最后一个指针
head->next = p;
}
ListNode* Reverse(ListNode* p) {
if (NULL == p) {return NULL;} //稳健性处理
if (p->next == NULL) { //说明是最后一个了
return p;
}
ListNode* fn = Reverse(p->next);
p->next->next = p;
p->next = NULL; //这句没有就死循环了 关键是让最左边的结点的next为NULL
return fn;
}
递归算法的关键其实就是逆向遍历,最后一个返回p,倒数第二个就p->next->next=p ,等于是p->next(最后一个结点)->next(的next)=p(指向倒数第二个结点)。然后倒数第二个结点指向倒数第三个结点,依次遍历。
最后让头结点指向原最后一个结点p。
打印链表倒数第k个结点:遍历递减法
void Print_rev_K(LinkList head,int k) {
assert(head != NULL);
ListNode* p = head->next;
int n = 0;
while (p != NULL) {
n++;
p = p->next;
}
//如果k大于n的话 说明就错误了
if (k > n) {
printf("%s\n", "超过长度");
return;
}
p = head->next;//要把p指针重新赋值成head->next
while (n > k) {
n--;
p = p->next;
}
//结束完p就在倒数第k个位置了
printf("%d \n", p->data);
}
打印链表倒数第k个结点:双指针法
void Print_rev_K2(LinkList head, int k) {
assert(head != NULL);
//双指针同时遍历
ListNode* p = head;
ListNode* pre = head->next;
while (p!=NULL&&k--) {
p = p->next;
}
//这时候p指向第k个 但如果p等于NULL的话 就说明k肯定大于长度了
if (NULL == p) {
printf("%s\n", "超过长度");
return;
}
//如果能走到这里说明没有遍历完
while (p->next!= NULL) {
//遍历到最后一个
p = p->next;
pre = pre->next;
}
printf("%d \n", pre->data);
}
思路就是保持p和pre之间的间隔,与倒数第一个和倒数第k个的间隔一样。然后让p和pre同时移动,让p走到倒数第一个,pre就是倒数第k个。
返回链表中间结点(偶数右):快慢指针
ListNode* returnMid(LinkList head) {
assert(head != NULL);
ListNode* slow = head->next;
ListNode* fast = head->next;
while (fast != NULL && fast->next != NULL) {
fast = fast->next->next;
slow = slow->next;
}
printf("%d\n", slow->data);
return slow;
}
判断链表是否有环,若有环,则环的起点是哪
void IsRound(LinkList head) {
assert(head != NULL);
ListNode* slow = head->next;
ListNode* fast = head->next;
do {
fast = fast->next;
if (fast != NULL) {
//如果fast==NULL 说明是奇数已经走完了
fast = fast->next;
}
slow = slow->next;
} while (fast != NULL && slow != fast);
if (NULL == fast) {
printf("%s\n", "没有环");
return;
}
//走到这里就是有环 并且肯定是因为slow==fast停止的
slow = head->next;
while (fast != slow) {
fast = fast->next;
slow = slow->next;
}
printf("有环,%d是起点\n", slow->data);
}
判断两个链表是否有相交点(差值法)
void IsIntersection(LinkList headA, LinkList headB) {
assert(headA != NULL && headB != NULL);
int na, nb;
na = nb = 0;
ListNode* pa, * pb;
pa = headA->next;
pb = headB->next;
while (pa != NULL) {na++; pa = pa->next;} //na是headA的长度
while (pb != NULL) {nb++;pb = pb->next;} //nb是headB的长度
pa = headA->next;
pb = headB->next;
while (na > nb) {na--;pa = pa->next;} //(若有相交点)让pa和pb与相交点的距离相同
while (nb > na) {nb--;pb = pb->next;}//(若有相交点)让pa和pb与相交点的距离相同
while (pa!=NULL&&pb!=NULL&&pa != pb) {
pa = pa->next;
pb = pb->next;
}
//pa==pb的时候 就是相交的点或者一起走到NULL
if (pa != NULL) {
printf("%s", "相交");
printf("%d\n", pa->data);
}
else {
printf("%s", "不相交");
}
}
判断两个链表是否有相交点(遍历法)
void IsIntersection2(LinkList headA, LinkList headB) {
assert(headA != NULL && headB != NULL);
ListNode* pa = headA->next;
ListNode* pb = headB->next;
while (pa != pb) {
//要么是pa==pb 要么是pa==pb=NULL
pa = pa == NULL ? headB->next : pa->next;
pb = pb == NULL ? headA->next : pb->next;
}
//pa和pb都等于NULL就说明没有相交 pb=pb不等于NULL才是有相交
if (pa == NULL) {
printf("没有相交点");
}
else {
printf("%d是相交点", pa->data);
}
}
这个算法其实和上面的差值法差不多 都是利用前置距离相同指向相交点,但是遍历法不需要知道两个链表的长度,差值法需要首先计算长度,而遍历法是直接遍历就行。
链表向右旋转
void RotateK(LinkList head,int k) {
ListNode* p = head;
int n = 0;
while (p->next != NULL) {
p = p->next;
n++;
}
//这时候p指向最后一个元素
k = k % n;
if (k == 0) { return; } //如果k等于0就不需要操作
p->next = head->next; //链表最后一个指向第一个元素
while (k--) {
p = p->next;
//这时候的p指向改变后的最后一个元素
}
head->next = p->next;//头节点指向p的next
p->next = NULL;
}
链表以x分割且保持相对顺序不变(不带头结点)
ListNode* Partition(LinkList head,int x) {
assert(head != NULL);
//给head设置一个头结点
ListNode* pa = (ListNode*)malloc(sizeof(ListNode));
if (NULL == pa) { exit(1); }//如果空间不足就终止程序
//遍历的任务交给pc pa就是接收指针
ListNode* pc = head;
pa->next = NULL;
head = pa;
//再设置一个headB链表
ListNode* headB = (ListNode*)malloc(sizeof(ListNode));
if (NULL == headB) { exit(1); }//如果空间不足就终止程序
headB->next = NULL;
ListNode* pb = headB;
//------------------上面的代码大致就是初始化两个带有头结点的链表,pc就是遍历原始链表
//根据x分割
while (pc != NULL) {
if (pc->data > x) {
pb->next = pc;
pb = pb->next;
pc = pc->next;
pb->next = NULL; //很重要不然链表无结尾
}
else {
pa->next = pc;
pa = pa->next;
pc= pc->next;
pa->next = NULL;
}
}
//此时pa在headA链表的最后 将其指向headB的第一个结点
pa->next = headB->next;
//稳健性 释放两个头结点 返回head
pa = head;
head = head->next;
free(pa);
pa = NULL;
free(headB);
headB = NULL;
return head;//返回一个无头节点的head
//这时候head就是无头结点的了
}
链表快速排序(无头结点)
//快排函数
void Quick_Sort(LinkList head) {
//虽然可以直接用Quick_Sort_Fun完全可以实现一样的功能,但是用户不知道要传end等于NULL 所以用户只会传head 所以需要写两个函数 用户只需传head 而end等于NULL是我们默认传的
assert(head != NULL);
Quick_Sort_Fun(head, NULL);
}
//快排递归
void Quick_Sort_Fun(ListNode* begin, ListNode* end){
if (begin == end || begin->next == end) {
//如果begin和end重合或相差一位就退出
return;
}
ListNode* left = begin;
ListNode* right = begin->next;
int item = begin->data; //对比的数字
while (right != end) {
if (right->data < item) {
//如果找到小的 就把他们放到前面
left = left->next;
Swap(&right->data, &left->data);//交换函数在下面
}
//向后走
right = right->next;
}
Swap(&left->data, &begin->data);//最后交换第一个结点data和left结点的data
//以left为分割 继续递归
Quick_Sort_Fun(begin, left);
Quick_Sort_Fun(left->next, end);
}
//交换函数
void Swap(int *pa, int *pb) {
int tmp = *pa;
*pa = *pb;
*pb = tmp;
}
有头结点只需要把快排函数中的Quick_Sort_Fun(head, NULL);改成Quick_Sort_Fun(head->next, NULL); 要保证第一个参数为链表的第一个结点
随机链表的复制
RandomListNode* Random_Copy(RandomListNode* head) {
assert(head != NULL);
RandomListNode* p = head->next;
while (p != NULL) {
RandomListNode* s = (RandomListNode*)malloc(sizeof(RandomListNode));
if (NULL == s) { exit(1); } //如果空间不足就终止程序
s->next = p->next;
p->next = s;
s->data = p->data;
s->random = NULL;//
p = s->next;//最后让p移动
}
//-------------上面代码等于是复制了一份在每个节点之后
//将复制后的结点的random设置
p = head->next;
while (p != NULL) {
p->next->random = p->random == NULL ? NULL : p->random->next;
p = p->next->next;
}
//创建一个新链表,把复制的都给他
RandomListNode* headB = (RandomListNode*)malloc(sizeof(RandomListNode));
if (NULL == headB) { exit(1); } //如果空间不足就终止程序
headB->next = NULL;
RandomListNode* pb = headB;//不能是headB的next headB->next是NULL;
p = head->next;
while (p != NULL) {
pb->next = p->next;//把复制的元素给pb
p->next = p->next->next;//让p跨越复制的元素
pb = pb->next; //pb走一步
pb->next = NULL;
p = p->next;
}
return headB;
}
链表判断是否回文(数组检查)
bool IsPalindrome(LinkList head) {
assert(head != NULL);
ListNode* p = head->next;
int n = 0;
while (p != NULL) {n++;p = p->next;}//计算链表长度
int* arr = (int*)malloc(sizeof(int) * n);//开辟数组
if (NULL == arr) { exit(1); } //如果空间不足就终值程序
p = head->next;
n = 0;
while (p != NULL) {
arr[n] = p->data;
n++;
p = p->next;
}
bool res = true;//默认是回文
int left = 0;
int right = n - 1;
while (left++ < right--) {//left<right对比完再++ --
if (arr[left] != arr[right]) {
res = false;
break;//如果有一对不一样就返回false
}
}
//稳健性 释放arr空间
free(arr);
arr = NULL;
return res;
}
链表判断是否回文(局部逆置法)
大致就是将链表的右侧逆置,判断和左侧是否一样
bool IsPalindrome2(LinkList head) {
assert(head != NULL);
ListNode* slow = head->next;
ListNode* fast = head->next;
while (fast->next != NULL && fast->next->next != NULL) {
//fast->next != NULL 是 fast->next->next != NULL的前提条件,顺序不能变
fast = fast->next->next;
slow = slow->next;
}
//slow是中间结点(偶数中左)
ListNode* p = slow->next;
ListNode* after = NULL;
slow->next = NULL;//将slow假想成头结点 slow的next置空
//遍历后面的结点并头插进入slow
while (p != NULL) {
after = p->next;//先有一个指针往后走
p->next = slow->next;
slow->next = p;
p = after;
}
//--------这时候逆置已经完成了,就差对比左右是否对应
bool res = true;
ListNode* left = head->next;
ListNode* right = slow->next;
while (right != NULL) {
//从右边遍历到后面 如果都和前面一样的话就是回文
if (left->data != right->data) {
res = false;
break;
}
left = left->next;
right = right->next;
}
return res;
}