[数据结构]——线性表总结(c语言代码实现)爆肝两万字!_c语言线性表小结

void SListPushBack(SLTNode** pphead, SLTDataType x)//尾插
{
SLTNode* newnode = BuySListNode(x);//创建一个新结点。

if (\*pphead == NULL)
{
	\*pphead = newnode;
}
//这里判断一下传过来的这个头指针是不是指向NULL,如果是指向NULL的,说明单链表上还没有结点(因为头指针初始化为NULL),在这个状态下尾插,就可以直接让单链表的头指针指向这个新结点。
else
{
	//定义一个找尾节点的指针,从头指针这里开始遍历。
	SLTNode\* tail = \*pphead;
	while (tail->next != NULL)
	{
		tail = tail->next;
	}
	tail->next = newnode;
    //找到了尾节点直接在后面插入新创建的结点就行了。
}

}

void SListPopFront(SLTNode** pphead)//头删
{
SLTNode* next = (*pphead)->next;//创建一个指针变量将头结点的next指向的地址保留一下。
free(*pphead);
*pphead = next;

//还有个思路:其实也是道理也是一样的。
//SLTNode\* tmp = \*pphead;
//\*pphead = tmp->next;
//free(tmp);

}

void SListPopBack(SLTNode** pphead)//后删;
{
//用双指针,一个指向尾巴的前一个结点,一个指向尾巴。
SLTNode* prev = NULL;
SLTNode* tail = *pphead;
if (*pphead == NULL)//这个是空链表的情况,直接返回。
{
return;
}
else if ((*pphead)->next==NULL)//这个是链表只有一个结点的情况。
{
free(*pphead);
*pphead = NULL;
}
else//这个是链表有两个及两个以上结点的情况。
{
while (tail->next != NULL)
{
prev = tail;
tail = tail->next;
}
free(tail);
prev->next = NULL;
}

}

SLTNode* SListFind(SLTNode* phead, SLTDataType x)//查找
{
SLTNode* cur = phead;
while (cur != NULL)
{
if (cur->data == x)
return cur;
cur = cur->next;
}
return NULL;
}

void SListInsert(SLTNode** pphead,SLTNode* pos,SLTDataType x)//在pos前面的位置上插入x。
//因为需要上传结点参数,所以一般都与查找接口一齐使用。先查找返回对应的结点,然后进行插入。
{
SLTNode* newnode = BuySListNode(x);
if (*pphead == NULL)//空链表时候的插入情况
{
*pphead = newnode;
newnode->next = NULL;
}
else if (pos == *pphead)//头插情况
{
newnode->next = *pphead;
*pphead = newnode;
}
else
{
SLTNode* tmpt = *pphead;
while (tmpt->next != pos)//一直向后遍历
{
tmpt = tmpt->next;
}
tmpt->next = newnode;
newnode->next = pos;
}

}

void SListErase(SLTNode** pphead,SLTNode*pos)//删除pos位置上的值。
//因为需要上传结点参数,所以一般都与查找接口一齐使用。先查找返回对应的结点,然后进行插入。
{
SLTNode* cur = *pphead;
if (*pphead == NULL)//如果是空链表就直接返回。
{
return;
}
else if(pos==*pphead)//头删情况。
{
*pphead = cur->next;
free(cur);
cur = NULL;
}
else
{
while (cur->next != pos)
{
cur = cur->next;
}
cur->next = pos->next;//这样就直接将pos结点删除了。
free(pos);
}

}


##### 3,测试文件


这个单链表可以进行测试测试。



#pragma once
#include"SList.h"//引一下头文件
test1()
{
SLTNode* phead = NULL;;
SListPushBack(&phead, 1);
SListPushBack(&phead, 2);
SListPushBack(&phead, 3);
SListPrint(phead);
//想在1前面插入0.
SLTNode* pos = SListFind(phead, 1);//先找到1
if (pos != NULL)
{
SListInsert(&phead, pos, 0);
SListPrint(phead);
}
else
printf(“没有找到您要找的位置”);
//想要删除链表中的2.
SLTNode* pos2 = SListFind(phead,2);//先找到2
if (pos2 != NULL)
{
SListErase(&phead, pos2);
SListPrint(phead);
}
else
{
printf(“没有找到您要删除的元素”);
}
}
int main()
{
test1();
return 0;
}


##### 4,带头链表和不带头链表


上面的单链表是属于不带头的单链表。接下来讲一下带头的单链表。


![img](https://typora-csdn.oss-cn-qingdao.aliyuncs.com/E%24SD435~%25%24GGL~OLS1QW7YQ.png)


带头链表的好处:



> 
> 尾插:还要判断首结点是不是空,如果是空,那么还得改变头指针的指向(原来是NULL,现在要指向这个newnode,修改了),所以这个就需要二级指针了。(如果要对原先的头指针进行修改,那么就需要使用二级指针。),而带头链表直接在head后面加就行了。不需要使用二级指针。尾插的判断会更简单。
> 
> 
> 



> 
> 头删:也是一样的,普通的单链表要找到d2,让头指针指向d2才行,这样也需要改变plist,使用二级指针。但是带头链表,指针直接找到d2了。(将d2的地址保存到head的next中)直接让head上的头指针指向d2就行了。这样实际上也没有改变头指针的地址。注意:头指针和带头结点是不一样的,头指针只有一个地址,没有next,而带头结点是有next的,这就可以放直接放地址,直接指向一个地方而不用改变自身的地址。而头指针改变指向的对象的话,就会改变自身的地址。
> 
> 
> 


注意:头结点的head是不能存数值的(不能用头结点来存链表的长度)。这样是不规范的。链表中存的都是整数还好说,如果是其他的数据类型,例如char,double就尴尬了。


单链表的尾删,插入,删除的时间复杂度都是O(n),他们都是需要找到指定节点的前一个结点。


解决方案:双向链表(有后继,有前驱)


#### 三,双向链表


用代码实现一下带头的双向循环链表。


##### 1,头文件



#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#pragma once
typedef int LTDataType;

typedef struct ListNode
{
struct ListNode* next;
struct ListNode* prev;
int data;
}ListNode;
//声明一下接口
ListNode* ListInit();//初始化。
void ListDestory(ListNode* plist);//销毁
void ListPushBack(ListNode* plist, LTDataType x);//尾插
ListNode* BuyListNode(LTDataType x);//创建新结点。
void ListPrint(ListNode* plist);//打印
void ListPushFront(ListNode* plist, LTDataType x);//前插
void ListPopFront(ListNode* plist);//前删
void ListPopBack(ListNode* plist);//尾删
ListNode* ListFind(ListNode* plist, LTDataType x);//查找
ListNode* ListInsert(ListNode* pos,LTDataType x);//在pos前面插入
ListNode* ListErase(ListNode* pos);//删除pos出的结点
void ListModify(ListNode* pos, int num);//在pos这里更改数据;


##### 2,C文件



#define _CRT_SECURE_NO_WARNINGS 1
#include"List.h"

ListNode* BuyListNode(LTDataType x)//创建新结点
{
ListNode* newnode = (ListNode*)malloc(sizeof(ListNode));
newnode->next = NULL;
newnode->data = x;

return newnode;

}

ListNode* ListInit()//初始化
{
ListNode* phead =BuyListNode(0);//这个时候phead就已经是头结点了
phead->next = phead;
phead->prev = phead;
return phead;
//使用举例:ListNode* phead = ListInit();创建哨兵位头结点。
}

void ListPushBack(ListNode* phead, LTDataType x)//后插
{
assert(phead);//这个链表最起码应该是带个head,所以肯定不能是空。
ListNode* newnode = (ListNode*)BuyListNode(x);
ListNode* tail = phead->prev;//先定义一下尾结点。
newnode->prev = tail;
newnode->next = phead;
tail->next = newnode;
phead->prev = newnode;
//这个进行的操作没有直接影响到指针(结点地址)本身,而改变的是next和prev,所以不用引二级指针。
}

void ListPrint(ListNode* phead)//打印
{
ListNode* cur = phead->next;
while (cur != phead)
{
printf(“%d “, cur->data);
cur = cur->next;
}
printf(”\n”);
}

void ListPushFront(ListNode* phead, LTDataType x)//前插
{
assert(phead);
ListNode* newnode = BuyListNode(x);
newnode->next = phead->next;
newnode->prev = phead;
phead->next->prev = newnode;
phead->next = newnode;
}

void ListPopFront(ListNode* phead)//前删
{
assert(phead);//判断确保不能链表不能为空。
assert(phead->next != phead);//判断确保链表不能只有头结点。(因为至少得有两个结点方法才能成立)
ListNode* first = phead->next;
ListNode* second = phead->next->next;
phead->next = second;
second->prev = phead;
free(first);
first = NULL;
}

void ListPopBack(ListNode* phead)//尾删
{
assert(phead);//判断确保不能链表不能为空。
assert(phead->next != phead);//判断确保链表不能只有头结点。(因为至少得有两个结点方法才能成立)
ListNode* tail = phead->prev;
ListNode* tailPrev = phead->prev->prev;
tailPrev->next = phead;
phead->prev = tailPrev;

free(tail);
tail = NULL;

}

ListNode* ListFind(ListNode* phead, LTDataType x)//查找,返回结点
{
assert(phead);
ListNode* cur = phead->next;
while (cur->data != x)//进行遍历。
{
cur = cur->next;
if (cur->next == phead)
return NULL;
}
return cur;
}

void ListModify(ListNode* pos, int num)//将pos这里的结点的data改成num
{
pos->data = num;
}

ListNode* ListInsert(ListNode* pos, int x)//在pos前面插入
{
assert(pos);
ListNode* prev = pos->prev;
ListNode* newnode = BuyListNode(x);
prev->next = newnode;
newnode->prev = prev;
newnode->next = pos;
pos->prev = prev;

}

ListNode* ListErase(ListNode* pos)//删除pos出的结点
{
assert(pos);
ListNode* prev = pos->prev;
ListNode* next = pos->next;
prev->next = next;
next->prev = prev;
free(pos);
pos = NULL;
}

void ListDestory(ListNode* phead)//销毁
{
assert(phead);
ListNode* cur = phead->next;
while (cur != phead)//遍历,保存下一个,销毁前一个。
{
ListNode* next = cur->next;
free(cur);
cur = next;
}
free(phead);//最后销毁头结点
phead = NULL;
}


#### 四,栈


##### 1,栈的相关概念


###### (1),定义



> 
> 栈是一种特殊的线性表,只允许在固定的一端进行插入和删除元素,进行数据插入和删除操作的一段称为栈顶,另一端称为栈底。
> 
> 
> 


###### (2),实现方式的选择


用数组实现:


用数组实现栈可以完美的避开顺序表的缺点(避免在表头插入和删除需要移动大量的数据,因为如果用数组来实现栈的话,就是将顺序表的尾巴当作的栈顶,压栈和出栈对应着尾插和尾删,这就很方便)(而且数据扩容也不是特别的频繁。)


用链表实现:


1,双链表:链尾表示栈顶,如果要删除的话,必须要找到最后一个还有倒数第二个结点,也还可以。


2,单链表:链头表示栈顶,开始的时候,让栈顶和栈底都指向同一个,压栈就是头插,出栈就是头删。举个头删的例子:保存栈顶的后一个,然后将原先的栈顶销毁掉,让栈顶指向栈顶的后一个。这样出栈和入栈都是O(1)。


所以如果用链尾表示栈顶,那么就用双链表。


如果用链头表示栈顶,那么就用单链表。


这些方法都是可以的,你能写出来就行。


推荐:数组


1,(单链表增和删有些繁琐)


2,数组的cpu缓存的命中率要更高一点。(预加载)(因为数组的地址是连续的)


如果是连续的内存,cpu要访问的话要把他加载到寄存器。我会直接预加载一大块东西。第一个命中了,第二个命中的概率就会很多高了。链表就不会了,命中了第一个,由于内存不是连续的,所以想要预加载到第二个就会有些困难。


##### 2,头文件



#pragma once//防止头文件重复引用
#include<stdio.h>
#include<stdbool.h>
#include<stdlib.h>
#include<assert.h>
typedef int STDataType;

typedef struct Stack
{
STDataType* a;//搞一个数组
int top;//栈顶的位置
int capacity;//栈的容量
}Stack;

void StackInit(Stack* ps);//初始化
void StackDestory(Stack* ps);//销毁单链表
void StackPop(Stack* ps);//删除
void StackPush(Stack* ps, STDataType x);//插入
STDataType StackTop(Stack* ps);//栈顶
int StackSize(Stack* ps);//输出大小
bool StackEmpty(Stack* ps);//是否为空


##### 3,C文件



#include"Stack.h"//引一下头文件

void StackInit(Stack* ps)
{
assert(ps);
if (ps->a == NULL)
{
printf(“malloc失败了”);
exit(-1);
}
ps->a = (Stack*)malloc(sizeof(STDataType) * 4);//整个4个对应的数据类型的空间
if (ps->a == NULL)
{
printf(“malloc失败了”);
exit(-1);
}
ps->capacity = 4;
ps->top = 0;
//如果这里的栈顶top是赋值的0,那么栈顶指向的永远都是栈的最后一个数据的下一位置。
//因为一开始栈里面还没有数据的时候,top是0,在栈的第一个位置,有了数据之后,top就得指向第二个位置了。(用数组实现的)
//如果top给到-1,那么top指向的数值就是栈的最后一个数值。在这里代码给的是0.
}

void StackDestory(Stack* ps)
{
assert(ps);
free(ps->a);
ps->a = NULL;
ps->top = ps->capacity = 0;
}

void StackPush(Stack* ps,STDataType x)//入栈
{
assert(ps);
//如果满了那么就需要扩容量
if (ps->top == ps->capacity)
{
STDataType* tmp = (STDataType*)realloc(ps->a, ps->capacity * 2 * sizeof(STDataType));
if (tmp == NULL)
{
printf(“realloc失败了”);
exit(-1);
}
else
{
ps->a = tmp;
ps->capacity = 2 * ps->capacity;
}

}
ps->a[ps->top] = x;
ps->top++;

}
void StackPop(Stack* ps)//出栈
{
assert(ps);
assert(ps->top > 0);//如果栈空了,那么就直接终止程序报错。
ps->top–;
}
STDataType StackTop(Stack* ps)//返回栈顶
{
assert(ps);
assert(ps->top > 0);//如果栈空了,就会访问到栈的前一个出界的随机元素了,肯定报错。
return ps->a[ps->top - 1];
}
int StackSize(Stack* ps)//输出大小
{
assert(ps);
return ps->top;
}
bool StackEmpty(Stack* ps)//是否为空
{
//assert(ps);
return ps->top == 0;
}


##### 4,测试文件



#define _CRT_SECURE_NO_WARNINGS 1
#include"Stack.h"
int main()
{
Stack st;
StackInit(&st);

StackPush(&st, 1);
StackPush(&st, 2);
StackPush(&st, 3);
StackPush(&st, 4);
//遍历方法:(属于栈的)
while (!StackEmpty(&st))
{
	printf("%d ", StackTop(&st));
	StackPop(&st);
}
printf("\n");

StackDestory(&st);//只要有初始化,就得有销毁。

}


#### 五,队列


##### 1,队列的相关概念


###### (1),定义



> 
> 队列定义:队列是只允许在一端插入数据,然后在另一端进行删除数据操作的特殊线性表。队列的特点就是先进先出。
> 
> 
> 


![QQ图片20210928233105](https://typora-csdn.oss-cn-qingdao.aliyuncs.com/QQ%E5%9B%BE%E7%89%8720210928233105.png)
在队头出数据,在队尾进数据。


###### (2),实现方式的选择


实现方法:


数组:不太好,因为队头出数据的时候需要挪动了链表。


链表:操作就是头删和尾插。(完美的运用的单链表的优点,头删和尾插的效率都很高)。


下面的代码定义了一个(指针)结构体{head和tail},这样就不用二级指针了。


这里是不需要带头结点的,双向链表中的带头结点的目的是解决单链表中的二级指针的问题的,带头结点中的next和prev可以存取地址。头指针和带头结点是不一样的,头指针只有一个地址,没有next,而带头结点是有next的,这就可以放直接放地址,直接指向一个地方而不用改变自身的地址。而头指针改变指向的对象的话,就会改变自身的地址。(单链表和双向链表)


而队列:有指针结构体,在一个结构体存储着两个指针,一个是head还有一个是tail指针。有这两个指针也就不需要改变头结点的指针了,也就不需要二级指针了。


注意有两个结构体,一个结构体是关于指针的结构体,还有一个结构体是关于结点的结构体。如果向函数中传的参数是结点的话,那么就需要用二级指针,但是传的参数是指针的话,就只需要传指针就行了。


那为什么单链表的时候不用这个结构体来用tail呢?有tail之后尾插确实很简单,但是尾删还是很困难。单链表就是直接传的head,进行二级指针,这样就能改变指针的值。


##### 1,头文件



#define _CRT_SECURE_NO_WARNINGS 1

#pragma once
#include<stdio.h>
#include<stdbool.h>
#include<stdlib.h>
#include<assert.h>
typedef int QDataType;

typedef struct QueueNode//这里是结点的结构体
{
struct QueueNode* next;
QDataType data;
}QNode;

typedef struct Queue//这里是指针的结构体。(有这个结构体就不需要二级指针了)
{
QNode* head;
QNode* tail;
}Queue;

void QueueInit(Queue* pq);//初始化
void QueueDestory(Queue* pq);//销毁单链表
void QueuePop(Queue* pq);//队头出
void QueuePush(Queue* pq, QDataType x);//队尾入
QDataType QueueFront(Queue* pq);//取队首的元素。
QDataType QueueBack(Queue* pq);//取队尾的元素。
int QueueSize(Queue* pq);//取队列的长度
bool QueueEmpty(Queue* pq);//判断队列是不是空


##### 2,C文件



#include"Queue.h"//引一下头文件

void QueueInit(Queue* pq)//初始化
{
assert(pq);
pq->head = pq->tail = NULL;

}
void QueuePop(Queue* pq)//队头出
{
assert(pq);
assert(pq->head);//判断栈是否满了。
//删除之前要先保存下一个。

//为了防止后面的问题:
if (pq->head->next == NULL)//如果队列中就一个数据
{
	free(pq->head);
	pq->head = pq->tail = NULL;
}
else
{
	QNode\* next = pq->head->next;
	free(pq->head);
	pq->head = next;
}

//如果没有前面的if的话就会有一个问题,就是如果free掉最后一个。head和next都会指向NULL(这个是没有问题的,但是tail就会指向已经被释放掉的空间,成为了野指针)

}
void QueuePush(Queue* pq, QDataType x)//队尾入
{
assert(pq);
QNode* newnode = (QNode*)malloc(sizeof(QNode));//malloc一个新结点
if (newnode == NULL)
{
printf(“mallco fail\n”);
exit(-1);
}
newnode->next = NULL;
newnode->data = x;
if (pq->tail == NULL)//用tail和head比较都行。if(pq->head==NULL)
{
pq->tail = pq->head = newnode;
}
else
{
pq->tail->next = newnode;
pq->tail = newnode;
}
}

QDataType QueueFront(Queue* pq)//取队首的元素。
{
assert(pq);
assert(pq->head);//保证至少有一个元素。
return pq->head->data;
}

QDataType QueueBack(Queue* pq)//取队尾的元素。
{
assert(pq);
assert(pq->head);//保证至少有一个元素
return pq->tail->data;
}

int QueueSize(Queue* pq)//取队列的长度
{
int size = 0;
QNode* cur = pq->head;//定义一个指针。
while (cur)
{
size++;
cur = cur->next;
}
return size;
}

bool QueueEmpty(Queue* pq)//判断队列是不是空
{
assert(pq);
return pq->head == NULL;
}

void QueueDestory(Queue* pq)//销毁单链表
{
assert(pq);
QNode* cur = pq->head;
while (cur)
{
QNode* next = cur->next;//保存下一个结点
free(cur);
cur = next;
}
pq->head = pq->tail = NULL;

}


##### 3,测试文件



#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include"Queue.h"
#include"Queue.h"

int main()
{
Queue q;
QueueInit(&q);
QueuePush(&q, 1);
QueuePush(&q, 2);
QueuePush(&q, 3);
QueuePush(&q, 4);
QueuePop(&q);
//遍历(满足现进先出的特点)
while (!QueueEmpty(&q))
{
printf(“%d “, QueueFront(&q));
QueuePop(&q);
}
printf(”\n”);
return 0;
}


#### 六,栈和队列的补充


栈的实现:


底层可以用数组,也可以用链表,可以单链表,双链表,带头,不带头都可以。


如果用链表实现的话,推荐用单链表。


用链表的头这一端来做栈顶。这样录数据就相当于头插,出数据就相当头删。O(1)。


不要用链表的尾这一端来做栈顶,这样录数据好进入。但是出数据的时候就不容易删除了(要找到它的前一个,必须要遍历)O(n).


但是:


更推荐用数组来实现。录入数据就是直接将数据赋值过去,然后top下标向后移动就行了。出数据就直接top–,最后的数据就没有了(两个操作都是在数组的末尾进行的)可以用动态的数组,也可以用静态的数组。但是推荐用动态的,静态的给多了用不完,给少了不够用。


也就是在定义结构体的时候多一个capacity,然后不够用了就扩容。realloc。


队列的实现:


推荐还是用链表,


用数组的话就不太好:录入数据的时候还好说,但是出数据的时候出一个数据就需要将后面的数据整体的往前移动。效率就低了。


用链表:从链表的尾处入数据,在链表的头处出数据。(两个指针)都是O(1),把优点集合两个了。(三个方向都挺好的,在头上插,在头上删,在尾上插。但是在尾上删除就不好了O(n))。


#### 七,二叉树


##### 1,树的概念


结构:任何一棵二叉树都有三部分,根结点,左子树,右子树。


重点算法:分治算法:分而治之,大问题分成类似子问题,子问题再分成子问题。直到子问题不可再分割。


遍历方法:


前序(先根遍历):先访问根结点,然后左子树,最后右子树。


中序(中根遍历):先访问左子树,然后根结点,最后右子树。


后序(后根遍历):先访问左子树,然后右子树,最后根节点。


满二叉树:每一层都是满的。


![img](https://typora-csdn.oss-cn-qingdao.aliyuncs.com/1726R~80GP2%5BNIO%5D%24GN_SPO.png)
设总的结点数是N个,共h层。那么定满足2^h-1=N.


完全二叉树: 设树的高度是h,则h-1层都是满的,最后一层不是满的,但是最后一层从左到右都是连续的。


![img](https://typora-csdn.oss-cn-qingdao.aliyuncs.com/ZLW%5BO6O%5BRFMU%7bDNL%7d2PH2TI.png)
设最后一层还差x个结点才能构成满二叉树,那么满足2^h-1-x=N。


##### 2,前序,中序,后序遍历


代码实现:(为了演示三种遍历顺序,在main函数中直接定义了一棵二叉树。(具体见下面main函数))


![QQ图片20210929081616](https://typora-csdn.oss-cn-qingdao.aliyuncs.com/QQ%E5%9B%BE%E7%89%8720210929081616.png)

#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
typedef char BTDataType;
typedef struct BinaryTreeNode//这个结点定义的是左子树,右子树,数据。
{
struct BinaryTreeNode* left;
struct BinaryTreeNode* right;
BTDataType data;
}BTNode;

void PrevOrder(BTNode* root)//前序
{
if (root == NULL)
{
printf("NULL “);
return;
}
printf(”%c ", root->data);
PrevOrder(root->left);
PrevOrder(root->right);

}
void InOrder(BTNode* root)//中序
{
if (root == NULL)
{
printf("NULL ");
return;
}

InOrder(root->left);
printf("%c ", root->data);
InOrder(root->right);

}
void PostOrder(BTNode* root)//后序
{
if (root == NULL)
{
printf("NULL ");
return;
}

PostOrder(root->left);
PostOrder(root->right);
printf("%c ", root->data);

}

int main()//直接定义了一棵二叉树
{
BTNode* A = (BTNode*)malloc(sizeof(BTNode));

A->data = 'A';
A->left = NULL;
A->right = NULL;

BTNode\* B = (BTNode\*)malloc(sizeof(BTNode));
B->data = 'B';
B->left = NULL;
B->right = NULL;

BTNode\* C = (BTNode\*)malloc(sizeof(BTNode));
C->data = 'C';
C->left = NULL;
C->right = NULL;

BTNode\* D = (BTNode\*)malloc(sizeof(BTNode));
D->data = 'D';
D->left = NULL;
D->right = NULL;

BTNode\* E = (BTNode\*)malloc(sizeof(BTNode));
E->data = 'E';
E->left = NULL;
E->right = NULL;

A->left = B;
A->right = C;

(1)Python所有方向的学习路线(新版)

这是我花了几天的时间去把Python所有方向的技术点做的整理,形成各个领域的知识点汇总,它的用处就在于,你可以按照上面的知识点去找对应的学习资源,保证自己学得较为全面。

最近我才对这些路线做了一下新的更新,知识体系更全面了。

在这里插入图片描述

(2)Python学习视频

包含了Python入门、爬虫、数据分析和web开发的学习视频,总共100多个,虽然没有那么全面,但是对于入门来说是没问题的,学完这些之后,你可以按照我上面的学习路线去网上找其他的知识资源进行进阶。

在这里插入图片描述

(3)100多个练手项目

我们在看视频学习的时候,不能光动眼动脑不动手,比较科学的学习方法是在理解之后运用它们,这时候练手项目就很适合了,只是里面的项目比较多,水平也是参差不齐,大家可以挑自己能做的项目去练练。

在这里插入图片描述

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化学习资料的朋友,可以戳这里无偿获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

  • 19
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值