超多注释助你手撕指针与链表

在这里插入图片描述

结构体也叫做更复杂的结构类型,只要类型能够做的事情,结构体也能做

struct person{
    char name[20];
    int age;
    char gender;
    float height;
}
struct person Yu; 
struct person *p;
p = &Yu;
  • 直接引用:Yu.name
  • 间接引用:p->name
  • 所以复杂结构体能够用指针间接访问,可以用变量直接访问
  • 结构体以所占空间最大的作为对齐,比如float占4字节是最大的,那么申请的内存会是4的整数倍。
// 匿名结构体只能用一次
struct node{
    double a;
    char b;
    int c;
}
// 结构体内存占用图如下

共用体用法

使用共用体实现ip转整数的功能

  • xxx.xxx.xxx.xxx -> 每个xxx都是0-255
#include <stdio.h>
union IP{
    // 共用体共用一片内存!!!
    // ip和四个char型变量共用一片内存空间
    struct{
        unsigned char a1;
        unsigned char a2;
        unsigned char a3;
        unsigned char a4;
    }ip;
    unsigned int num;
};
// 判定电脑是大端机还是小端机
int is_little(){
    int num = 1;
    return ((char*)(&num))[0];
}
//小端机会输出0

int main(){
    union IP p;
    char str[100] = {0};
    int arr[4];
    // 判断是不是小端机
    printf("%d\n",is_little());
    while(~scanf("%s",str)){
        sscanf(str,"%d.%d.%d.%d",arr,arr+1,arr+2,arr+3);
        // 数据是小的存在低位,如果存储变成1234,数据会变化
        p.ip.a1 = arr[3];
        p.ip.a2 = arr[2];
        p.ip.a3 = arr[1];
        p.ip.a4 = arr[0];
        printf("%u\n",p.num);

    }
    return 0;
}
  • 网络传输的时候要看主机字节序,网络字节序和主机字节序可能会不一样,所以需要进行一定的转码。

##指针和地址

  • 什么是地址?相当于生活中的家庭地址。64位操作系统地址标号是0- 2 64 − 1 2^{64}-1 2641, 32位操作系统地址标号是0- 2 32 − 1 2^{32}-1 2321 。在32位操作系统中,最多只能给4GB的内存进行标记。64位基本没有上限。
  • 变量是用来存储值的,不同类型的变量存储不同的值,指针变量也是变量。指针变量能够接收的值是地址。
  • 指针变量能够接收到的地址是一个指向某种数据类型的地址int *p= &a 定义指针变量,定义完之后进行*p 可以取值。假设a = 123 打印输出*p = 123
  • **一个指针变量占几个字节呢?**在32位操作系统中占4字节,64位操作系统中占8字节。 现在默认是64位操作系统的字节。int ** q = &p q是指向指针p的指针,就是指向指针的指针。浮点数和int都是8byte,其实是可以用指针进行互相表示的,int的指针也能存储float的地址。精度不会有变
  • 一个指针变量占几个字节,一定要严谨!
  • 指针的等价转换:*p = a* (从数组上说)p + 1==&p[1](从结构体上说) p->filed = (*p).filed = a.filed
  • int *f(int) 这是返回值是一个指针的函数 int(*f)(int) 是一个函数指针
#include <stdio.h> 
struct Data{
    int x,y;
};
// a[2]是用结构体定义的数组
#define P(func){\
    printf("%s = %d\n",#func,func);\
}

int main(){
    struct Data a[2], *p = a;
    a[0].x = 0, a[0].y = 1;
    a[1].x = 2, a[1].y = 3;
    // 不想多说,但还可以更多
    P(a[1].x);
    P(p[1].x);
    P((*(p+1)).x);
    P((p+1)->x);
    P((&a[1])->x);
    P((&p[1])->x);
    P(((&a[0])+1)->x);
    P(((&p[0])+1)->x);
    P((*((&a[0])+1)).x);
    P((*((&p[0])+1)).x);
}

##链表

  • C语言地址能够唯一地标记某个对象。数组下标其实本质上也是一种地址。下标是一种相对地址的概念,或者是用引用实现搜索链表下一个的东西,表示数组元素距离第一个元素的距离。我们已经知道指针相当于是某个数据的引用int*p = &a,就是一种引用。

  • 只要在结构中添加了指针域,就可以把所有的东西串成一链。

  • 链表包含数据域和指针域。链表中的每个节点,通过指针域的值,形成一个线性结构。查找节点O(n), 插入和删除是O(1) 不适合快速的定位数据,适合动态地插入和删除数据的应用。单向链表只有一个指针域,可以一直向后,不能向前查找。双向链表可以前后查找,每个节点有两个指针。链表就是链表,虽然说链表加了一个指针域变成两个指针域可以是双向链表,也可以是二叉树,但是在思维逻辑上说,数和链表其实不太沾边,差很多,只是在实现的时候比较像。Java中没有指针,但是有引用。

  • 几种经典的链表实现方法

    // 第一种
    struct Node{
        // 构造函数,构造Node函数对象的时候希望传入一个data数据
        Node(int data) :data(data),next(NULL) {}
        int data;
      	//指针域  
        Node* next;
    };
    
    int main(){
        Node *head =NULL;
        // 通过这些构造链表, 不纠结语法
        // new 能够返回对象的一个地址
        head = new Node(1);
        head->next = new Node(2);
        head->next->next= new Node(3);
        head->next->next->next= new Node(4);
        Node *p = head;
        while(p !=NULL){
            printf("%d->",p->data);
            p = p->next;
        }
        printf("\n");
        return 0;
    }
    
  • // 第二种
    // 分别实现的是数据域和指针域
    int data[10];
    // 指针域,对应的data[1]存储1节点的值,next[1]存储它指向下一个节点的地址(在这里是数组的相对地址)
    int next[10];
    
    // 在index节点处添加地址(数组下标)为p的节点,地址为p的节点存储的是value
    void add(int ind, int p,int value){
        // 以下这句话保证链表能够在中间被插入,没有下面这句话就没有办法在链表中间插入
        // 只能在末尾插入,或者是插入之后,这个元素就是最后的值,它之后的元素都消失了
        next[p] = next[ind];
        // p是这阵
        next[ind] = p;
        data[p] = value;
        return ;
    }
    
    int main1(){
        int head = 3;
        // 构造链表
        data[3] = 0;
        add(3,5,1);
        add(5,2,2);
        add(2,7,3);
        add(7,9,100);
        add(5,6,7);
    
        int p = head;
        while (p!=0){
            printf("%d->",data[p]);
            p = next[p];   
        }
        printf("\n");
        return 0;
    // 语言其实并不重要,实现方式思维的理解才重要
    }
    

链表应用的经典场景(leetcode除外哈哈哈)

链表的应用场景1:
  • 操作系统内的动态内存分配。操作系统是把不同的内存碎片串成了一个链表,碎片与碎片之间通过指针连接。底层的动态内存分配是用链表进行管理的。

  • 缓存算法:缓存是高速设备对低速设备的一种称呼。缓存就是容硬盘上读取东西,方便进行使用,是优化读取数据的一种方法。内存中建立缓存空间。CPU在取数据的时候有两种方法,最原始的就是从硬盘取,但是速度很慢。如果把cpu经常用的数据存储到内存中之后,就变成了缓存空间,这样读取数据会快很多。cpu在读取数据的时候先到缓存中取一下,之后再到硬盘中取数据。这样比较快。

  • 缓存中的数据是如何存储的呢?就是哈希链表,或者说是链表的方法。单链表并不是底层实现的方式。新加数据就是直接用链表插入,删除就直接指针去除就可以了。就像下面的图所示
    - [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-26cg88j4-1615825757227)(C:\Users\sakura\Desktop\奋斗吧cv\门徒\week1链表\Snipaste_2021-03-04_21-12-04.png)]

  • LRU缓存淘汰算法:如果查找到了,就是缓存命中。如果查不到的话,就会淘汰掉最早放在缓存中的数,让要查找的数组。

Leetcode链表

  • 链表一直都是一种绵里藏针的东西,不要看它就是一个铁索连环,但是指针的移动真的会让人头大。下面终于开始leetcode的学习了。
LeetCode #141 环状链表
  • 采用双指针做法,使用使用快慢指针,快指针一次向前2个节点,慢指针一次向前1个节点。主要基于以下重要结论。
    有环的链表中 快指针和慢指针最终一定会在环中相遇
    无环的链表中 快指针会率先访问到链表尾 从而终结检测过程
class Solution {
public:
    bool hasCycle(ListNode *head) {
        // 如果只有一个节点以下,就直接返回了,没有环
        if(head == NULL || head->next == NULL) return false;
        // 快慢指针定义
        ListNode *p = head,*q = head;
        do{
            // p每次走一步,q每次走两步
            p = p->next;
            q = q->next->next;
        }while(p != q && q && q->next);
        // 用下面两句话会超时
        // 不可以用两个if语句一起写return 否则会出现编译不过的情况
        if ((q==nullptr) || (q->next ==nullptr)) return 0;
        else return 1;
        // 或者可以返回下面的这句话,下面这句话意思是,如果q和q->next都不是空地址,就直接返回1
        // return q && q->next;
    }
};

LeetCode #142 环状链表II
  • 双指针同上,具体解题方法如下图:

  • 考虑快慢指针第一次相遇的情况(设此时慢指针走的路程为x)-> 指定一个指针p放置在链表头部(p每次向前1个节点)-> 再走一个路程为x的长度-> 慢指针到达了2x的位置->指针p到达了x的位置->慢指针和p相遇了->往前回放一下 在环的入口开始一起向前直到相遇
    在这里插入图片描述


class Solution {
 public:
  ListNode *detectCycle(ListNode *head) {
    // 如果没有环,返回空地址
    if (head == nullptr) return nullptr;
    // 这一行也可以让p从头开始,q从头指针的下一个节点开始
    ListNode *p = head, *q = head;
    // 返回空节点
    if (q->next == nullptr) return nullptr;
    // 因为第一步的时候两个节点是一个位置
    do {
      p = p->next;
      q = q->next->next;
      // 判断所有的东西都不是空,也不是相等
    } while (p != q && q && q->next);
    // 如果都是空了,就返回空节点,说明没有环
    if (q == nullptr || q->next == nullptr) return nullptr;
    p = head;
    // 相遇在环的起始位置,q还需要再走a步。
    while (p != q) p = p->next, q = q->next;
    return q;
  }
};


LeetCode #202 快乐数
  • 思路:转化为判断链表是否有环的问题
    收敛性的证明
    32位int的表示正整数大概是21亿( 2 31 − 1 2^{31}-1 2311)

    • 在这个范围内 各位数字平方和最大的数是1999999999 和为730
    • 根据鸽巢原理(pigeonhole’s principle,也译作抽屉原理)在730次循环后必定出现重复

    从收敛性性质可以知道,这个快乐书是有环的。那么只要找到它能不能到1,或者说

class Solution {
 public:
  int getNext(int x) {
    int z = 0;
    // 把所有的值进行每一位相乘
    while (x) {
      z += (x % 10) * (x % 10);
      x /= 10;
    }
    return z;
  }

  bool isHappy(int n) {
    // p只当前数字
    int p = n, q = n;
    do {
      // p,q进行快慢行进
      p = getNext(p);
      q = getNext(getNext(q));
      // 链表判定是不是空
    } while (p != q && q != 1);
	// 如果出现循环又没有1,就返回false了	
    return q == 1;
  }
};
LeetCode #206 翻转链表
  • 使用虚拟头结点来进行头插法
  • 进行递归翻转
// 递归翻转
class Solution {
 public:
  ListNode *reverseList(ListNode *head) {
    // 如果头指针是空或者只有一个节点,直接返回头指针
    if (head == nullptr || head->next == nullptr) return head;
    // 先记录后一个节点的地址,翻转head和next
    ListNode *tail = head->next, *p = reverseList(head->next);
    // 链表头尾翻转
    head->next = tail->next;
    // 接过去了
    tail->next = head;
    // p是用来记录翻转之后链表的头结点,返回之后就不动了,一直都是返回同样的值
    return p;
  }
};
class Solution {
public:
    ListNode* reverseList(ListNode* head) {
        // 只有一个节点,直接返回头结点
        if(head==nullptr || head->next==nullptr) return head;
        // 定义前一个节点,要翻转的节点还有翻转节点的下一位
        ListNode *pre = nullptr, *cur = head,*p = head->next;
        // 如果现在要翻转的节点不是空的话,空的节点说明到了最底了
        while(cur ){
            cur->next= pre;
            pre = cur;
            // && 的短路做用
            (cur=p) && (p = p= p->next);
            // 等价与上面那句话
            // if(cur=p){
            //     p= p->next;
            // }
            // p= p->next);     
        }
        return pre;
    }
};

LeetCode #92 反转链表II
  • 使用虚拟头结点,防止头结点因为链表的翻转而改变
// 先找到要翻转的头一位,之后调用翻转头n个节点的函数,翻转之后再让一节点指针指向被翻转之后的链表的头结点
class Solution {
 public:
// 前面的这个定义符号相当于int,是一种struct类型
// 递归翻转节点之后的n个元素
  ListNode *reverseN(ListNode *head, int n) {
    // 递归出口
    if (n == 1) return head;
     // 调用递归,把翻转的尾节点定义一下
    ListNode *tail = head->next, *p = reverseN(head->next, n - 1);
     // 链表翻转
    head->next = tail->next;
    tail->next = head;
    return p;
  }

  ListNode *reverseBetween(ListNode *head, int m, int n) {
    // 定义一个链表,这个是通过line_10 定义的,传入一个变量x=0,之后定义头结点的指针
    // 把p指向头结点的地址,方便返回
    ListNode ret(0, head), *p = &ret;
    // 翻转之后的节点
    int cnt = n - m + 1;
    // 先跑到要应该翻转的链表的前一个位置,这个是虚头
    // 注意是--m,就是跑到要翻转的节点的前一个位置
    while (--m) p = p->next;
    // 进行后n位置的链表翻转
    p->next = reverseN(p->next, cnt);
    return ret.next;
  }
};

LeetCode #25 K个一组翻转链表
  • 先判断是否有K个元素 然后对这K个节点进行反转 最后拆装一下首尾部分
  • 三段论解题法:
    • 首先完成翻转n个链表的函数
    • 之后完成判定链表能不能翻转的函数,并把能够翻转的链表翻转了
    • 最后完成不断循环翻转的函数
class Solution {
 public:

  ListNode *__reverseN(ListNode *head, int n) {
    if (n == 1) return head;
    // p是用来记录头结点的指针变量,就是翻转之后的头结点
    ListNode *tail = head->next, *p = __reverseN(head->next, n - 1);
    head->next = tail->next;
    tail->next = head;

    return p;
  }
  // 这个是定义这个函数?
  ListNode *reverseN(ListNode *head, int n) {
    ListNode *p = head;
    int cnt = n;
    // 判断是不是足够n个节点
    while (--n && p) p = p->next;
    // 要返回头结点
    if (p == nullptr) return head;

    return __reverseN(head, cnt);
  }
  ListNode *reverseKGroup(ListNode *head, int k) {
    // p是要翻转的数组的前一位,q是要翻转数组的第一位
    // 下面这一句话是初始化
    ListNode ret(0, head), *p = &ret, *q = p->next;
    // 这个翻转的表达式不是空的时候,等于q表示没有发生翻转,因为只有翻转之后量表的两个指针才会有位置不同的变化
    // 最骚最骚的
    while ((p->next = reverseN(q, k)) != q) {
      p = q;
      q = p->next;
    }

    return ret.next;
  }
};

LeetCode #61 旋转链表
  • 先让指针走到链表的最后一位,并计算出链表的长度。

  • 计算出链表要向右移动多少位。链表在右移的时候需要进行一下计算

    • k%= cnt; k = cnt-k;
  • 将头指针向右移动k位,之后,把k-1位的那个节点断开,变成空。这样就起到右移节点的目的了。

class Solution {
 public:
//  先找到最后一个节点,把链表形成环,之后找到要断掉的指针,断掉就变成了一个链表
  ListNode *rotateRight(ListNode *head, int k) {
    // 处理一下空链表
    if (head == nullptr) return nullptr;
    int n = 1;
    ListNode *p = head;
    // 先走到最后一位
    // n用来记录链表长度
    while (p->next) p = p->next, n += 1;
    // 把头尾连起来
    p->next = head;
    // k需要处理,原地转圈得去掉
    k %= n;
    // 这个是剩余的要走的步数,这个是循环右移动
    k = n - k;
    // 循环左移就是k
    while (k--) p = p->next;
    head = p->next;
    // 断开指针
    p->next = nullptr;

    return head;
  }
};

LeetCode #24旋转链表
  • 这道题我现在还是有点懵逼,尽力画了,理解了原理,但是自己要写还是不太会
class Solution {
public:
    ListNode* swapPairs(ListNode* head) {
        // 递归出口
        if (head == nullptr || head->next == nullptr) {
            return head;
        }
        ListNode* newHead = head->next;
        // 调换的是newhead后面的那两个节点
        head->next = swapPairs(newHead->next);
        newHead->next = head;
        return newHead;
    }
};

在这里插入图片描述


LeetCode #19 删除链表的倒数第N个节点
  • 思路,定义两个指针,一个指针先超前走n步,之后另一个指针向前走,
    当前一个指针走到null的时候,后面的指针就到了倒数第n个节点
  • 要特别注意一下几个代码
  • n-- 是代表向前走了n步,之后p是走到要删除节点的前一位才可以
class Solution {
public:
// 思路,定义两个指针,一个指针先超前走n步,之后另一个指针向前走,
// 当前一个指针走到null的时候,后面的指针就到了倒数第n个节点
    ListNode* removeNthFromEnd(ListNode* head, int n) {
        // 头结点有可能变化,要变一下虚拟头结点
        ListNode ret(0,head);
        ListNode *p= &ret,*q= head;
        // 向前走了n步
        while(n--) q = q->next;
        // 走到了要删除节点的前一位
        while(q) p = p->next,q = q->next;
        // 删除节点
        p->next = p->next->next;
        // 返回虚拟头结点
        return ret.next;
    }
};

LeetCode #83 删除排序链表中的重复节点
  • 先判断链表是不是空!!!!!!!!!!!!!!!!!!!!!
  • 定义一个指针p,如果p和p->next指向的值是一样的话,就把p指向下一位的下一位
  • 如果值不一样,就直接向后走一步
class Solution {
public:
    ListNode* deleteDuplicates(ListNode* head) {
        // 头结点不删,也不变,直接保留
        // 第一句空的要保留,不然又会报错了。在写题目的时候一定要保证
        // 空指针的判断!!!!!!!!!!!!!
        if (head==nullptr) return nullptr;
        ListNode* p = head;
        while(p->next) {
            if(p->val == p->next->val){
                p->next = p->next->next;
            }else{
                p = p->next;
            }
        }
        return head;
    }
};

LeetCode #82 删除排序链表中的重复节点II
  • 首先头结点可能会变,需要设置一个虚拟头结点
  • 在进行节点删除的时候,要注意p从虚拟头结点开始出发
  • 剩下的请自行看代码
class Solution {
 public:
//  穿进去的是头地址
// 虚拟头结点要用在链表头地址可能改变的情况
// 多了它之后会少了很多边界条件的判断
  ListNode *deleteDuplicates(ListNode *head) {
    // 首先设置虚拟头结点,p指向虚拟头
    ListNode ret(0, head), *p = &ret, *q;
    // 有下一个节点的时候,先判断是否产生了重复
    while (p->next) {
      // 这两个判定的顺序是不能更改的
      if (p->next->next && p->next->val == p->next->next->val) {
        q = p->next->next;
        // 出现值重复的情况,就是q下一个值和下一个下一个的值相等,除去多余的指针
        while (q && q->val == p->next->val) q = q->next;
        // 
        p->next = q;
      } else {
        // 没有出现重复的情况
        p = p->next;
      }
    }
    return ret.next;
  }
};

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值