1.单链表
主要操作
首先
#include<stdio.h>
struct node{
int data;
struct node*next;
};
find(struct node*head,int k,struct node**p){
//链表第k个结点的地址赋值给*p
链接: 为什么一个指针变量作为形参传递给一个函数后,在函数体内改变这个指针的指向,函数结束后这个指针的指向没有发生变化?
这篇文章讲解了二级指针和指针引用的问题。
//二级指针or指针的引用
void find(struct node*head,int k,struct node**p){//链表第k个结点的地址赋值给p
//f1k是否合法
if(k<0){
*p=NULL;
return;
};
//f2初始化
*p=head;
int i=0;//令指针p指向哨兵结点计数器初始值为0;
//f3找第k个结点
while(*p!=NULL&&i<k){
*p=(*p)->next;
i=i+1;
}
}
void search(struct node*head,int item,struct node**p){
//在链表中查找字段值为item的结点并返回其指针
void search(struct node*head,int item,struct node**p){//在链表中查找字段值为item的结点并返回其指针
//s1初始化
p=head->next;
//遍历
while(*p!=NULL&&(*p)->data!=item){
*p=(*p)->next;
}
}
void delete(struct node*head,int k){
//删除链表中第k个结点
void delete(struct node*head,int k){//删除链表中第k个结点
//d1k是否合法
if(k<1)return;
//d2找到第k-1个结点,由p指向
struct node *p=NULL;
find(head,k-1,&p);
if(p==NULL||p->next==NULL)return;
//d3删除第k个结点
struct node*q=p->next;
p->next=q->next;
free(q);//释放q存储空间避免内存泄漏
}
void insert(struct node*head,int k,int item){
//在链表中第k个结点后插入字段值为item的结点
void insert(struct node*head,int k,int item){//在链表中第k个结点后插入字段值为item的结点
//i1是否合法
if(k<0)return;
//i2找到第k个结点
struct node *p=NULL;
find(head,k,&p);
if(p==NULL)return;
//i3插入
struct node *s=(struct node*)malloc(sizeof(struct node));//生成新结点
s->next=p->next;
p->next=s;
}
2.循环链表
循环链表和单向链表的判断问题
struct node{
int id;
struct node*next;
};
//带哨兵节点的循环链表
struct node*create(int n){
struct node*head=NULL,*p=NULL,*tail=NULL;
head=(struct node*)malloc(sizeof(struct node));
for(int i=1;i<=n;i++){
p=(struct node*)malloc(sizeof(struct node));
p->id=i;
p->next=NULL;
if(head==NULL){
head=p;
tail=p;
}else{
tail->next=p;
tail=p;
}
}
tail->next=head;
return tail;
};
int ifempty(struct node*head){
if(head->next->next==head){
return 1;
}
return 0;
}
int iftail(struct node*p){
if(p->next==head){
return 1;
}
return 0;
}
int ifempty_dan(struct node*head){
if(head->next==NULL){
return 1;
}
return 0;
}
int iftail_dan(struct node*p){
if(p->next==NULL){
return 1;
}
return 0;
}
链接: 约瑟夫环问题
题目描述
n个人围成一圈,从第一个人开始报数,数到 m的人出列,再由下一个人重新从1开始报数,数到 m的人再出圈,依次类推,直到所有的人都出圈,请输出依次出圈人的编号。
输入格式
输入两个整数 n,m。
输出格式
输出一行 n个整数,按顺序输出每个出圈人的编号。
数组解法
#include<stdio.h>
int main(){
int n,m;
scanf("%d",&n);
scanf("%d",&m);
int a[n];
int i;
for(i=0;i<n;i++)
a[i]=0;
i=0;
int flag=n;
int count=1;
while(flag!=1){
if(count==m){
a[i]=1;
printf("%d ",i+1);
flag--;
count=0;
}
i=(i+1)%n;
if(a[i]!=1)count++;
}
return 0;
}
10 3
3 6 9 2 7 1 8 5 10
思考:一开始想通过纯计算得到那个人的位置,后来发现有可能中间有寄的,debug了;
然后我朋友和我说直接存然后遍历,有时间代价直接加一行判断就可以;
在i%x的这部分一定要x是模,而且加了之后就是0了;
如果是1开始的存储,取的时候i%(n-1),然后比如n=10,i%9,9%9=0,判断一手将i+1;
#include<stdio.h>
int main(){
int n,m;
scanf("%d",&n);
scanf("%d",&m);
int a[n+1];
int i;
for(i=1;i<=n;i++)
a[i]=0;
i=0;
int flag=n;
int count=1;
while(flag!=1){
if(count==m){
a[i+1]=1;
printf("%d ",i+1);
flag--;
count=0;
}
i=(i+1)%(n-1);
if(a[i+1]!=1)count++;
}
return 0;
}
链表解法
#include <stdio.h>
#include <stdlib.h>
struct node{
int id;
struct node*next;
};
struct node*create(int n){
struct node*head=NULL,*p=NULL,*tail=NULL;
for(int i=1;i<=n;i++){
p=(struct node*)malloc(sizeof(struct node));
p->id=i;
p->next=NULL;
if(head==NULL){
head=p;
tail=p;
}else{
tail->next=p;
tail=p;
}
}
tail->next=head;
return tail;
};
void josephus(struct node*tail,int n,int m,int*a){
struct node*head=tail->next,*p=head,*pr=tail;
int num=1,count=0;
int i=0;
while(count<n-1){
if(num==m){
pr->next=p->next;
*a=p->id;
a++;
free(p);
count++;
p=pr->next;
num=0;
}else{
pr=pr->next;
p=p->next;
}
num++;
}
*a=p->id;
free(p);
}
int main()
{
int m,n;
scanf("%d%d",&n,&m);
struct node*tail=create(n);
int a[n];
josephus(tail,n,m,a);
for(int i=0;i<n;i++)
printf("%d",a[i]);
return 0;
}
思考:在num++那里上面的else情况,一开始没有写到else里,没有考虑删除节点已经p指针后移了一次,重复后移造成问题;在num++写在前面和后面这个思考是只关心if的判断和p节点的对应的;在这里加入哨兵节点的话对题目不友好因为我们最后剩余一个节点再删除,并不会有删除空节点的情况,这个哨兵节点带来的好处在计数型题目中可以忽略掉徒增烦恼。
3.双向链表
#include<stdio.h>
struct dlnode{
int data;
struct dlnode*left,*right;
};
//带哨兵节点的创建双向链表
struct dlnode*create(int n){
struct node*head=(struct node*)malloc(sizeof(struct node));
head->data=0;
head->left=NULL;
head->right=NULL;
struct node*tail=head,*p=NULL;
for(int i=1;i<=n;i++){
p=(struct node*)malloc(sizeof(struct node));
p->data=i;
tail->right=p;
p->left=tail;
p->right=NULL;
tail=p;
}
tail=p->next;
tail=(struct node*)malloc(sizeof(struct node));
p->right=tail;
tail->left=p;
tail->right=NULL;
};
//不带哨兵节点的创建双向链表
struct dlnode*create_no(int n){
struct node*head,tail,p=NULL;
for(int i=1;i<=n;i++){
p=(struct node*)malloc(sizeof(struct node));
p->data=i;
p->left=NULL;
p->right=NULL;
if(head=NULL){
head=p;
tail=p;
}else{
tail->right=p;
p->left=tail;
p->right=NULL;
tail=p;
}
}
};
双向链表的节点删除
因为感觉下面交代详细了直接略去,主要分有没有哨兵节点做两种处理
顺序存储和链式存储的比较
1.空间效率上(ps这称呼不大贴切感觉)
顺序表事先确定,链表动态申请
2.时间复杂性上
基于下表的存取
顺序表O(1)
链表O(n)
插入和删除
顺序表O(n)
链表O(1)
所以在插入删除频繁的时候看表更合适,在基于序号存取频繁的时候还是顺序表合适
4.静态链表
例题:
有若干个盒子,从左到右依次编号为1,2,3,…n可以执行以下指令(保证X不等于Y):
L(X,Y)表示可以把盒子X移动到盒子Y的左边(如果已经则忽略);
R(XY)表示可以把盒子X移动到盒子Y的右边(如果X已经在Y的右边则忽略)
#include<stdio.h>
int data[7];
int left[7];
int right[7];
void create(int n){
//ps或者直接二维矩阵
//为了构建双向循环链表
for(int i=1;i<=n;i++){
data[i]=i;
left[i]=i-1;
right[i]=(i+1)%7;
}
//构建head节点
left[0]=6;
right[0]=1;
}
//删除盒子X
//因为这个结构不会出现左右缺乏的情况(循环了)所以不用考虑缺少的情况
void deletebox(int x){
right[left[x]]=right[x];
left[right[x]]=left[x];
}
//将x插入y的右边
void rxy(int x,int y){//打扫好走之前环境后,注意y之后的节点需要right(y)来找到最后改变
//先把周边环境打扫好
right[left[x]]=right[x];
left[right[x]]=left[x];
//改变y右边的节点
left[right[y]]=x;
//改变x的两个
left[x]=y;
right[x]=right[y];
//改变y
right[y]=x;
}
//将x到y左边
void lxy(int x,int y){
//打扫环境
right[left[x]]=right[x];
left[right[x]]=left[x];
//改变y左边节点
right[left[y]]=x;
//改变x
left[x]=left[y];
right[x]=y;
//改变y
//被插入的最后改变
left[y]=x;
}
void print(int a[]){
printf("%d ",right[0]);
int m=right[0];
for(int i=1;i<=5;i++){
m=right[m];
printf("%d ",m);
}
printf("\n");
}
int main(){
create(6);
print(data);
lxy(1,4);
print(data);
rxy(3,5);
print(data);
}
链表应用场景
鸿蒙系统部分代码,参阅感想
#include<stdio.h>
typrdef struct node{
struct node*left;
struct node*right;
}node;
//双向循环链表初始化
void list_init(node*list){
list->left=list;
list->right=list;
}
//将指定节点挂到链表头部
void list_add(node*no,node*list){
no->right=list->right;
no->left=list;
list->right->left=no;
list->right=no;
}
//将指定节点从链表删除,自己把自己摘掉
void list_del(node*no){
no->left->right=no->right;
no->right->left=no->left;
no->left=NULL;
no->right=NULL;
}
将链表插入和初始化部分摘出来,在多操作和复杂代码中减少代码量值得参考
打飞机
- #include"graphics.h",该文件在visual studio2019中下载
- 宽字符输出环境调配
- 参考源码
- res\歌曲
我拿来跑了一下,效果不错
![](https://img-blog.csdnimg.cn/9c5837c60c304258aea9a839460aa6bb.png
自己尝试复现:
主要链表操作,其他操作理解了感觉对我帮助不大
主要操作:move()子弹和敌机同时移动,遍历链表,对y进行全部修改
碰撞:子弹和敌机用x和y还有它俩的宽度和高度进行判别,敌机和飞机同理;
销毁:子弹和敌机碰撞则从两个链表分别删除这两个节点,敌机和飞机相撞则游戏结束
游戏逻辑:初始化,移动飞机,随机生成敌机,敌机和子弹移动,删除碰撞的,判断,循环
#pragma once
#include<stdio.h>
#include<stdlib.h>
typedef struct node {
int x, y;
int speed;
struct node* next;
}node;
typedef struct nodelist{
node * head;
node* tail;
}nodelist;
node* list_init(int x, int y, int speed) {
node* temp = (node*)malloc(sizeof(node));
temp->x = x;
temp->y = y;
temp->speed = speed;
temp->next = NULL;
return temp;
}
void list_insert(nodelist*list, int x, int y, int speed) {
if (list->head == NULL) {
list->head = list->tail = list_init(x, y, speed);
}
else {
list->tail->next = list_init(x, y, speed);
list->tail = list->tail->next;
}
}
void list_delete(nodelist* list, node* p) {
if (p == list->head) {
list->head = list->head->next;
free(p);
p = NULL;
return;
}
node* temp=list->head;
for (; temp; temp = temp->next) {
if (temp->next == p) {
if (p==list->tail) {
list->tail = temp;
}
temp->next = p->next;
free(p);
p->next = NULL;
return;
}
}
}
void link_dekete(nodelist* list) {
for (node* temp1 = list->head;temp1;) {
node* temp2 = temp1;
temp1 = temp1->next;
free(temp2);
}
list->head = list->tail = NULL;
}
5.链表经典问题
找单链表倒数第k个节点只给head,
链表中倒数第 k 个节点
解法1
找到最后一个节点往前找k-1次前驱,
temp0->next= =NULL;
temp1->next= =temp0;
tempk->next= =tempk_1
O(n2)
解法2
正着数第n-k+1个
时间复杂度为O(n);遍历两次链表
第一次求出n的值
第二次找到该结点
解法3
双指针,p,q
p指向第k个元素,q指向第一个元素
当p到结尾,q刚好到那个结点
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* struct ListNode *next;
* };
*/
struct ListNode* getKthFromEnd(struct ListNode* head, int k){
struct ListNode* q=head;int count=1;struct ListNode* p=head;
for(;p;p=p->next){
if(count==k){
break;
}
count++;
}
for(;p->next;p=p->next,q=q->next){}
return q;
}
删除倒数第k和找中间结点略微思考就可以
链表中间结点(具体题目具体分析)
我用的leecode的图很直观。
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* struct ListNode *next;
* };
*/
struct ListNode* middleNode(struct ListNode* head){
struct ListNode*slow=head,*fast=head;
while(fast!=NULL&&fast->next!=NULL){
slow=slow->next;
fast=fast->next->next;
}
return slow;
}
注意这里还有另外一种常用变形:
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* struct ListNode *next;
* };
*/
struct ListNode* middleNode(struct ListNode* head){
struct ListNode*slow=head,*fast=head;
while(fast->next!=NULL&&fast->next->next!=NULL){
slow=slow->next;
fast=fast->next->next;
}
return slow;
}
思考:一开始我真没想明白这是什么情况;
这么想,在奇数情况下,上面的判断只能生效fast->next这个判别所以和之前的等价;
在偶数情况下slow往后少一次;
这就造成对这个节点的后面的节点直接reverseList之后便于搞回文;否则的话用原来那个在偶数的时候要直接对这个节点操作,需要分类,变得麻烦。
变形三:
struct ListNode* middleNode(struct ListNode* head){
struct ListNode *slow = head, *fast = head;
while (fast != NULL) {
slow = slow->next;
fast = fast->next;
if (fast != NULL) {
fast = fast->next;
}
}
return slow;
}
奇数情况下,slow多往前走了一个
偶数情况下一样
链表相交问题
只相交一次;
题目
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* struct ListNode *next;
* };
*/
struct ListNode *getIntersectionNode(struct ListNode *headA, struct ListNode *headB) {
if(headA==NULL||headB==NULL)return NULL;
if(headA==headB)return headA;
struct ListNode*p1=headA,*p2=headB;
int L1=1,L2=1;
while(p1->next!=NULL){L1++;p1=p1->next;};
while(p2->next!=NULL){L2++;p2=p2->next;};
if(p1!=p2)return NULL;
if(L1>=L2){p1=headA;p2=headB;}
else{p1=headB;p2=headA;}
// while (L1 > L2) {
// p1 = p1->next;
// L1--;
// }
// while (L2 > L1) {
// p1 = p1->next;
// L2--;
// }
for(int i=0;i<abs(L1-L2);i++)
{p1=p1->next;}
while(p1!=p2){
p1=p1->next;
p2=p2->next;
}
return p1;
}
单链表判环问题
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* struct ListNode *next;
* };
*/
struct ListNode *getIntersectionNode(struct ListNode *headA, struct ListNode *headB) {
if(headA==NULL||headB==NULL)return NULL;
if(headA==headB)return headA;
struct ListNode*p1=headA,*p2=headB;
int L1=1,L2=1;
while(p1->next!=NULL){L1++;p1=p1->next;};
while(p2->next!=NULL){L2++;p2=p2->next;};
if(p1!=p2)return NULL;
if(L1>=L2){p1=headA;p2=headB;}
else{p1=headB;p2=headA;}
for(int i=0;i<abs(L1-L2);i++)
{p1=p1->next;}
while(p1!=p2){
p1=p1->next;
p2=p2->next;
}
return p1;
}
struct ListNode *detectCycle(struct ListNode *head) {
struct ListNode*fast=head,*slow=head;int pos=-1;
while(fast!=NULL&&fast->next!=NULL){
fast=fast->next->next;
slow=slow->next;
if(fast==slow){
pos=1;
}
}
if(pos==-1)return NULL;
int count=1;
while(slow->next!=fast){count++;slow=slow->next;}
struct ListNode*x=fast;
fast=x->next;
x->next==NULL;
return getIntersectionNode(head,fast);
}
//上面的超时了,可能是编译器对时间限制了没有ac
//下面是leetcode上面的快慢指针的数学方法
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* struct ListNode *next;
* };
*/
struct ListNode *detectCycle(struct ListNode *head) {
struct ListNode*fast=head,*slow=head;int pos=-1;
while(fast!=NULL&&fast->next!=NULL){
fast=fast->next->next;
slow=slow->next;
if(fast==slow){
pos=1;
break;
}
}
if(pos==-1)return NULL;
struct ListNode*ptr=head;
while(ptr!=slow){
ptr=ptr->next;
slow=slow->next;
}
return ptr;
}
反转链表
题目
我看的题解有三种方法,第三种妖魔化指针,看着挺奇特的
正常做法
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* struct ListNode *next;
* };
*/
struct ListNode* reverseList(struct ListNode* head) {
struct ListNode* prev = NULL;
struct ListNode* curr = head;
while (curr != NULL) {
struct ListNode* t= curr->next;
curr->next = prev;
prev = curr;
curr = t;
}
return prev;
}
判断单链表是否是回文
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* struct ListNode *next;
* };
*/
//反转链表reverselist
struct ListNode* reverseList(struct ListNode* head) {
struct ListNode* prev = NULL;
struct ListNode* curr = head;
while (curr != NULL) {
struct ListNode* nextTemp = curr->next;
curr->next = prev;
prev = curr;
curr = nextTemp;
}
return prev;
}
//链表中间结点middlenode
struct ListNode*middlenode(struct ListNode*head){
struct ListNode*fast=head,*slow=head;
while (fast->next != NULL && fast->next->next != NULL) {
fast=fast->next->next;
slow=slow->next;
}
return slow;
}
bool isPalindrome(struct ListNode* head){
if (head == NULL) {
return true;
}
//前半部分的尾结点和后半部分的头结点
struct ListNode*firstnodeend=middlenode(head);
struct ListNode*secondnodehead=reverseList(firstnodeend->next);
int flag=true;
struct ListNode*p1=head;
struct ListNode*p2=secondnodehead;
while(flag&&p2){
if(p1->val!=p2->val)flag=false;
p1=p1->next;
p2=p2->next;
}
return flag;
}
思考:两个节点不是回文啊,我靠迷糊了,我说怎么看结构都不对呢;然后leetcode的题解将链表复位了我觉得很有必要,毕竟在这个操作之后这个结构不应该改变。//同时记得fast->next!=NULL在前
重排链表结点
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* struct ListNode *next;
* };
*/
struct ListNode*reverseList(struct ListNode*head){
struct ListNode*pre=NULL,*cur=head;
while(cur){
struct ListNode*t=cur->next;
cur->next=pre;
pre=cur;
cur=t;
}
return pre;
}
struct ListNode*middleNode(struct ListNode*head){
struct ListNode*fast=head,*slow=head;
while(fast->next&&fast->next->next){
fast=fast->next->next;
slow=slow->next;
}
return slow;
}
void mergeList(struct ListNode*head1,struct ListNode*head2){
struct ListNode*temp1=NULL,*temp2=NULL;
while(head1&&head2){
temp1=head1->next;
temp2=head2->next;
head1->next=head2;
head1=temp1;
head2->next=head1;
head2=temp2;
}
}
void reorderList(struct ListNode* head){
struct ListNode*middle=middleNode(head);
struct ListNode*l2=reverseList(middle->next);
middle->next=NULL;
mergeList(head,l2);
}
思考:这个合并好打;
然后reorderList的话不要忽略了middle->next=NULL;
否则的话不符合mergeList的条件。
多项式相加
园子里的实现
思考:链表排序他的选择是简单选择排序;
这个算法的基本思想是遍历所有位置(可以把最后一个摘掉,因为最后一个后面没有了),然后将每个位置的值从后面的位置中选择,选择里面最小的值和它替换,(这里就有可能把相等在前面的换到后面,也就是不稳定),时间复杂度O(n2),空间复杂度
O(1)比如数组中这样实现
void selectsort(int A[],int n){
for(int i=0;i<n;i++){
int min=i;
for(int j=i+1;j<n;j++){
if(A[j]<A[min]){min=j;}
if(min!=i)swap(A[i],A[min]);
}
}
}
链表排序的最快排序方式是归并排序
题目
先看一下我们熟悉的数组归并排序(自顶向下)
int*B=(int*)malloc((n+1)*sizeof(int));
void merge(int A[],int low,int mid,int high){
for(int k=low;k<=high;k++)
B[k]=A[k];
for(int i=low,int j=mid+1,int k=low;i<=mid&&j<=high;k++){
if(B[i]<=B[j]){//保证稳定性
A[k]=B[i++]
}else{
A[k]=B[j++]
}
}
while(i<=mid){
A[k++]=B[i++];
}
while(j<=high){
A[k++]=B[j++];
}
}
void mergesort(int A[],int low,int high){
if(low<high){//程序出口
int mid=(low+high)/2;
mergesort(A,low,mid);
mergesort(A,mid+1,high);
merge(A,low,mid,high);
}
}
//两个递归在左右两边分别向上,形似二叉树
自顶向下的链表排序
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* struct ListNode *next;
* };
*/
struct ListNode*merge(struct ListNode*head1,struct ListNode*head2){
struct ListNode*dummyHead=malloc(sizeof(struct ListNode));
dummyHead->val=0;
struct ListNode*temp=dummyHead,*temp1=head1,*temp2=head2;
while(temp1&&temp2){
if(temp1->val<=temp2->val){//确保稳定性
temp->next=temp1;
temp1=temp1->next;
}else{
temp->next=temp2;
temp2=temp2->next;
}
temp=temp->next;
}
if(temp1){
temp->next=temp1;
}else if(temp2){
temp->next=temp2;
}
temp=dummyHead;
dummyHead=dummyHead->next;
free(temp);
return dummyHead;
}
struct ListNode* toSortList(struct ListNode* head, struct ListNode* tail) {
//观察题目有head=[]这种情况只有从开始就是空才进入
if(head==NULL){
return head;
}
//设计程序出口
if (head->next == tail) {
head->next = NULL;
return head;
}
struct ListNode *slow=head,* fast=head;
while(fast!=tail&&fast->next!=tail){
fast=fast->next->next;
slow=slow->next;
}
//下面是第三种找中间结点,下面的图是按照这个来的,帮助我理解递归的
// while (fast != tail) {
// slow = slow->next;
// fast = fast->next;
// if (fast != tail) {
// fast = fast->next;
// }
// }
struct ListNode* mid = slow;
return merge(toSortList(head, mid), toSortList(mid, tail));
}
struct ListNode* sortList(struct ListNode* head){
return toSortList(head,NULL);//不用找tail结点,很好的设计
}
对于这个排序的细节如下:
6.跳跃表
相比leetcode给出的动图我觉得还是zhu教授自己做的ppt比较好,上面的经常让我编程的时候,把一个节点看成多个结构。还是下面的直观。我看下面的打的
leetcode跳跃表
题目
跳跃表空间复杂度分析
第一行是n,第二行是n1/2直到第i行
n(二分之一的累加)=O(n)暂时不想写公式了凑乎吧
跳跃表时间复杂度
第i层比较期望是i/2累加
时间复杂度是O(logn)
查找插入删除的时间复杂度分析
插入删除都和查找一样这个算法上面就是插入,所以都是logn
好的空间结构设计带来时间的优越,感觉就好像特斯拉的猎豹姿态
实现方式有四联表,data,above,below,next,pre这样的块构成
实现方式还有
struct Skip_Node{
int data;
Skip_Node*next[];
}
///或者和leetcode上的结构设计下面:
typedef struct SkiplistNode {
int val;
int maxLevel;
struct SkiplistNode **forward;
} SkiplistNode;
typedef struct {
SkiplistNode *head;
int level;
} Skiplist;
rand()
RAND_MAX=0x7fff
在>>2之后是0x3fff
#define MAX(a, b) ((a) > (b) ? (a) : (b))
const int MAX_LEVEL = 32;
const int P_FACTOR = RAND_MAX >> 2;
typedef struct SkiplistNode {
int val;
int maxLevel;
struct SkiplistNode **forward;
} SkiplistNode;
typedef struct {
SkiplistNode *head;
int level;
} Skiplist;
SkiplistNode *skiplistNodeCreat(int val, int maxLevel) {
SkiplistNode *obj = (SkiplistNode *)malloc(sizeof(SkiplistNode));
obj->val = val;
obj->maxLevel = maxLevel;
obj->forward = (SkiplistNode **)malloc(sizeof(SkiplistNode *) * maxLevel);
for (int i = 0; i < maxLevel; i++) {
obj->forward[i] = NULL;
}
return obj;
}
void skiplistNodeFree(SkiplistNode* obj) {
//删除obj->forward的指针然后在删除obj,这样彻底
if (obj->forward) {
free(obj->forward);
obj->forward = NULL;
obj->maxLevel = 0;
}
free(obj);
}
Skiplist* skiplistCreate() {
Skiplist *obj = (Skiplist *)malloc(sizeof(Skiplist));
obj->head = skiplistNodeCreat(-1, MAX_LEVEL);
obj->level = 0;
srand(time(NULL));
return obj;
}
static inline int randomLevel() {
int lv = 1;
/* 随机生成 lv */
//这个表每加一层翻倍,那么说2的32次方最多肯定不超过32层
//而且int最多存储2^31-1的整数,所以稳当
while (rand() < P_FACTOR && lv < MAX_LEVEL) {
lv++;
}
return lv;
}
bool skiplistSearch(Skiplist* obj, int target) {
SkiplistNode *curr = obj->head;
for (int i = obj->level - 1; i >= 0; i--) {
/* 找到第 i 层小于且最接近 target 的元素*/
while (curr->forward[i] && curr->forward[i]->val < target) {
curr = curr->forward[i];
}
}
curr = curr->forward[0];
/* 检测当前元素的值是否等于 target */
if (curr && curr->val == target) {
return true;
}
return false;
}
void skiplistAdd(Skiplist* obj, int num) {
SkiplistNode *update[MAX_LEVEL];
SkiplistNode *curr = obj->head;
for (int i = obj->level - 1; i >= 0; i--) {
/* 找到第 i 层小于且最接近 num 的元素*/
while (curr->forward[i] && curr->forward[i]->val < num) {
curr = curr->forward[i];
}
update[i] = curr;
}
int lv = randomLevel();
if (lv > obj->level) {
for (int i = obj->level; i < lv; i++) {
update[i] = obj->head;
}
obj->level = lv;
}
SkiplistNode *newNode = skiplistNodeCreat(num, lv);
for (int i = 0; i < lv; i++) {
/* 对第 i 层的状态进行更新,将当前元素的 forward 指向新的节点 */
newNode->forward[i] = update[i]->forward[i];
update[i]->forward[i] = newNode;
}
}
bool skiplistErase(Skiplist* obj, int num) {
SkiplistNode *update[MAX_LEVEL];
SkiplistNode *curr = obj->head;
for (int i = obj->level - 1; i >= 0; i--) {
/* 找到第 i 层小于且最接近 num 的元素*/
while (curr->forward[i] && curr->forward[i]->val < num) {
curr = curr->forward[i];
}
update[i] = curr;
}
curr = curr->forward[0];//这时候肯定不为空,修改后可能为空,最后一个没找到
/* 如果值不存在则返回 false */
if (!curr || curr->val != num) {
return false;
}
for (int i = 0; i < obj->level; i++) {
//找一个提前的出口在高层没有该结点的时候直接break;很高效二千避免了curr可能为空的可能
if (update[i]->forward[i] != curr) {
break;
}
/* 对第 i 层的状态进行更新,将 forward 指向被删除节点的下一跳 */
//下面的更新update[i]是指那个结点然后它选择层数后面
update[i]->forward[i] = curr->forward[i];
}
skiplistNodeFree(curr);
/* 更新当前的 level */
while (obj->level > 1 && obj->head->forward[obj->level - 1] == NULL) {
obj->level--;
}
return true;
}
void skiplistFree(Skiplist* obj) {
for (SkiplistNode * curr = obj->head; curr; ) {
SkiplistNode *prev = curr;
curr = curr->forward[0];//0层肯定有所有结点
skiplistNodeFree(prev);
}
free(obj);
}
//暂时自己没打这是官方答案,看懂了但是free指针数组那我没懂需要查资料,freenode那里
在这里对static inline的想法
大致总结:inline是一种内联建议,编译器在考虑它内联占用内存不多的时候会采用,但是如果inline函数声明在.h文件中,会被多个源文件调用;
作者的意思是如果建议没有被采用那么就是普通调用在多个文件分开编译之后汇总的时候会出问题,但是加了static之后就相当于各自有各自的func,相当于在每个文件写一个 static func,在互相使用函数的时候不会冲突,不会有在多个文件定义互相使用冲突的现象。
但是我在评论中看到一个有意思的评论,这么说更容易理解如果只用inline的话,如果在头文件中各个文件冲突了,会自动强制inline内联,我直接查阅资料,菜鸟
菜鸟这么说的:
4、建议 inline 函数的定义放在头文件中
其次,因为内联函数要在调用点展开,所以编译器必须随处可见内联函数的定义,要不然就成了非内联函数的调用了。所以,这要求每个调用了内联函数的文件都出现了该内联函数的定义。
因此,将内联函数的定义放在头文件里实现是合适的,省却你为每个文件实现一次的麻烦。
声明跟定义要一致:如果在每个文件里都实现一次该内联函数的话,那么,最好保证每个定义都是一样的,否则,将会引起未定义的行为。如果不是每个文件里的定义都一样,那么,编译器展开的是哪一个,那要看具体的编译器而定。所以,最好将内联函数定义放在头文件中。
所以可以知道不一定,所以还是规矩地写static inline吧
有序顺序表的二分查找
//额在leetcode这次打竟然没有通过
整数反转
注意,int i=s+(e-s)/2;这里是为了防止溢出,其实它和i=(s+e)/2是一样的但是当处理到maxs+maxe的情况下可能int接收不了。这里好久没刷了忘记了,但是凭借习惯写出来了。
这里我测试了下leetcode的取值范围
int最大2147483647,是2^31-1,显然对应32位存储,大概是210的9次方
int最小值是-2147483648,是-2^31,也对应32位存储,大概是210的-9次方
在原来的值加一就可以:这里对应leetcode的整数反转
在C语言中%运算是取余运算不是取模运算,区别就在对负数的运算上面。
int reverse(int x){
int rev=0;
while(x){
if(rev<INT_MIN/10||rev>INT_MAX/10){
return 0;
}
int digit=x%10;
x/=10;
rev=rev*10+digit;
}
return rev;
}
顺序表的二分查找
我写的代码开逻辑和标准答案一样但是没跑出来,报错也很玄幻,所以我去在线代码编辑器测试一下。测试过程需要初始化数组,好久没打了忘了查一下补充到这里:参考的博文
下面是够我用的:
全部初始化为0:
int a[10]={0};
char str[10]="\0"; //等价于char str[10]={0};
初始化为其他值:
int a[10]={0,1,2,3,4,5,6,7,8,9};
char str[10]="Hello"; //也可以写成char str[10]={'H','e','l','l','o','\0'}
如果初始化列表包含数组a的所有元素,可以省略数组的长度:
int a[]={0,1,2,3,4,5,6,7,8,9};
多维数组在计算机中也是线性存储的,因此下面两种写法等价:
int a[2][5]={{0,1,2,3,4},{5,6,7,8,9}};
//int a[2][5]={0,1,2,3,4,5,6,7,8,9};
更深入的请参考上面的链接,博主讲的真的好
数组做参数的方法
这些每次记了就按照一种打然后,长时间不打就忘了,总结到这里。
a,和a[]是一样传指针的,具体和sizeof搭配理解在这里
然后特别详细的教学在这里
int search(int* nums, int numsSize, int target){
int s=0,e=numsSize-1;
int found=-1;
while(s<=e){//最后s=e时害的判断一次
int i=s+(e-s)/2;
//思考为什么不是i=(s+e)/2,0+8/2=4对没错,0+9/2=4没错是前面的那一个
//如果是1开始,1+9/2是5是中间的没错,如果是1+10/2是5没错是后面那个
//如果是i=(s+(s-e)/2)则0+8/2=4没错,如果是0+9/2没错是前面那个
//如果是1开始,1+8/2=是5是中间没错,如果是1+9/2=5是5没错是后面那个
//0 5
//2
//9
if(target>nums[i])s=i+1;
if(target==nums[i])return i;
if(target<nums[i])e=i-1;
}
return found;
}
思考:今天边搁抖音看车边写走神了写反了数组index是负数e和s然后,判断还写反了;代码还是要一次打完。
对malloc和free的思考
malloc创建于堆,而函数内指针变量存在于栈内,函数内所有变量都在栈内,函数调用结束,栈会弹空,而堆不会,堆和块有关,到时候再说。参考文章
但是在不同的编译器给出了不同的优化,比如编译器中负责这一块的glibc,有一个阈值,超过从系统一起释放,否则就会继续存储,参考文章
free之后那块空间里面的值改变,指针指向的地址不变,也就是指针值不变。下面是
二维指针的free,free之后被指向的指针为野指针(好好理解这句话,符合上面的含义)。
另外一个指针两次申请区域都是同一地址开始。
但是在codeblocks里面还有让我懵逼的东西
上面看到p[3]指向了特殊区域,
然后运行结果是
修改想看的参数然后再debug发现codeblocks偷懒,只把free掉的存储区和邻近的随便改个没那么邻近的地方,然后把隔了好几个的全部搞到含有特殊值的区域。。。。
而在在线编译器上直接说断错误,分编译器,但是leetcode这么写说明完全可以,编译器还不行啊。同时要注意一个指针申请了一块内存,然后另一个指针等于它,不能两次free。
#include <stdio.h>
#include<stdlib.h>
int main(void) {
int**p=(int**)malloc(sizeof(int*)*6);
for (int i = 0; i < 6; i++) {
p[i] = (int*)malloc(sizeof(int)*2);
p[i][0]=0;
p[i][1]=1;
}
for (int i = 0; i < 6; i++){
for (int j = 0; j < 2; j++){
printf("%d ",p[i][j]);
}
printf("\n");
}
int*q=p[0];
printf("\n");
free(p);
for (int i = 0; i < 6; i++){
for (int j = 0; j < 2; j++){
printf("%d ",p[i][j]);
}
printf("\n");
}
return 0;
}
比较好的C语言学习路线文章
lightly和C语言版本
我感觉自己编程怎么方便怎么来hh,
C89是ANSI组织在80年代制定的标准。
C90是ISO组织接受C89标准在90年代发布的标准,其实与C89一样。
C99是上述两个组织于90年代发布的标准。
C11是2011年发布的标准。
区别详细叙述
轻量级在线编译器lightly
7.对于三种链表的删除问题的思考
//对不带哨兵节点的删除节点算法
//单向链表:左边的下一个是右边,要考虑左边是否为空,左边为空,直接head=head->next;free§;
//循环链表:左边的下一个是右边,只有一个节点,它的左边的下一个是右边没有变化,如果右边的右边是它自己的话,直接free,不是的话左边的右边是它的右边
//双向链表:最左边节点的删除,左边的右边需要是右边,如果左边是NULL则head=head->next;free§;将head的左边置空,其他中间节点正常;最右边的节点的话需要改变tail同理
//对带哨兵节点的 链表删除算法
//单向链表:最左边为空的话它的左边的右边是它的右边和正常情况统一
//循环链表:和单向链表同理,在判断是否为空的时候需要复杂点head->next->next==head;
//双向链表加了左右两个哨兵节点进行了统一