杨老师数据结构单链表算法笔记

杨老师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;
}

猜你喜欢

转载自blog.csdn.net/weixin_55109830/article/details/128880090