数据结构与算法
0、做题宝典
1.做题逻辑
- 像人解题的思维去编程
- 再将这个思维用代码实现
- 先写出逻辑再Debug
- 最厚再加上各种特殊情况(完善健壮性)
1.先按照感觉写大致的逻辑
2.微调判断边界
3.加上特殊条件
4.让逻辑和代码变得更优雅
//当使用到循环处理时,先找出终止条件,再写出返回值
- 自己先做一遍 //简单方法
- 找一个题解,现学现卖 //优美方法
2.常用函数模板(建议记忆)
1.各个位数之和
计算一个多位整数的各位数字之和
int digitSum(int num){
int sum = 0;
while (num > 0){
sum += num % 10; //从尾部逐个加加
num /= 10;
}return sum;
}
2.声明二维数组
以bool型二维数组为例:
bool** vi = (bool**)malloc(sizeof(bool*)*m); //申请M行数组
for(int i = 0; i < m; i++){
vi[i] = (bool*)calloc(n,sizeof(bool)); //牛逼,calloc可以初始化
}
malloc申请后空间的值是随机的,并没有进行初始化,
而calloc却在申请后,对空间逐一进行初始化,并设置值为0;
3.变量和一维数组做出参
int** levelOrder(struct TreeNode* root, int* returnSize, int** returnColumnSizes){
//其中将变量和一位数组按照地址传递处理
//int* returnSize 实则想传出的是一个值
//int** returnColumnSizes 实则想传出的是一个一维数组
}
下面我们来处理他们
- 对于变量的操作
如果想让该值自加的话记得先括上括号
(*returnSize)++;
- 对于一维数组的操作
//先申请一维数组的空间
*returnColumnSizes = (int*)malloc(sizeof(int)*2000);
//给数组的每个元素赋值(记得先用括号括起来)
(*returnColumnSizes)[i++] = num;
1、基础知识
0.参考博文
参考《王道_数据结构》
1.数据结构:逻辑结构 / 物理结构
1.逻辑结构
逻辑结构是指数据对象中数据元素之间的相互关系
可具体分为以下四种关系
A:集合结构
数据元素除了同属于一个集合外,它们之间没有其他关系
B:线性结构
数据元素之间是一对一关系
C:树形结构
数据元素之间呈现一对多关系
D:图形结构
数据元素是多对多关系
2.物理结构
是指数据在内存中真实存放的结构
1.顺序结构
2.链式结构
2、基本数据结构/C实现
0. 基本数据结构
- 链表 双向链表
- 树 二叉树 二叉搜索树 最小生成树
- 栈 单调栈
- 队列 单调队列
- 堆(优先队列)
- 哈希表
- 图
有序集合
拓扑排序
最短路
欧拉回路
双连通分量
强连通分量
1. 代码实现
1. 链表
一串链表是一串结构体,可以拿一个结构体数组实现;
1. 链表结构体
struct ListNode{
int val; //数据域
struct ListNode *next; //指针域
}
1.1(结构体来实现)链式结构
C 代码实现 参考
//================线性表的单链表存储结构=================
typedef struct LNode {
ElemType data;//数据域
struct LNode *next;//指针域(运用了结构体嵌套)
}LNode,*LinkList; //给结构体起了两个别名,分别以:LNode* p\LinkList p的方式来声明结构体指针。
LNode* p = (LNode*)malloc(sizeof(LNode));//给节点分配内存。
free(p); //释放链表
1.2删除链表
- 删除某个链表元素
- 删除整个链表
void Clear_LinkList(struct LinkNode* head)
{
if (head == NULL) return;
//先清空链表,是不清空头节点的,因此从第一个有数据的节点开始释放
struct LinkNode* curNode = head->next;
while (curNode != NULL)
{
//先保住下一个节点的位置
struct LinkNode* nextNode = curNode->next;
free(curNode);
curNode = nextNode;
}
//释放头结点
free(head); //先还是后释放head都可
//头结点指针置空
head = NULL;
}
2. 树
0. 基本概念
-
满二叉树
一个二叉树,如果每一个层的结点数都达到最大值,则这个二叉树就是满二叉树。也就是说,深度为k(k>=1)且有(2^k)-1个结点的二叉树就是满二叉树
-
完全二叉树
若设二叉树的深度为h,除第 h 层外,其它各层 (1~h-1) 的结点数都达到最大个数,第 h 层所有的结点都连续集中在最左边,这就是完全二叉树。
-
最大堆
什么是最大堆和最小堆?最大(小)堆是指在树中,存在一个结点而且该结点有儿子结点,该结点的data域值都不小于(大于)其儿子结点的data域值,并且它是一个完全二叉树(不是满二叉树)。
1. 树的结构体
struct TreeNode {
int val; //数据域
struct TreeNode *left; //左子树 (运用了结构体嵌套)
struct TreeNode *right; //右子树 (运用了结构体嵌套)
};
1.1(链式结构)//没啥意义呀?
C代码实现 参考
#include <stdio.h>
#include <stdlib.h>
#define TRUE 1
#define FALSE 0
typedef int elemtype;
typedef struct tNode* tree;
typedef struct tNode {
elemtype elem;
tree left; //左子树的结构体
tree right; //右子树的结构体
}tNode;
计算树的节点个数
//明确函数的功能:返回传入树的节点个数
//定好尾头:尾:当传入的节点尾NULL时 头:1 + count(t->left) + count(t->right)
int count(tree t) //tree等价于struct TreeNode*
{
if (t == NULL) return 0;
return 1 + count(t->left) + count(t->right);
}
3. 栈(stack)
一个数组栈是一个结构体内包函的数组在增加;
一个链栈是一串结构体;
1. 栈结构体
/*1.数组实现*/
typedef struct stack{
int data[MAXSIZE];
int top; //用于栈顶指针
}Stack;
/*2.链表实现*/
typedef struct stack{
int data;
struct stack *next;
}Stack;
1.1(数组实现)
typedef struct
{
int data[MAXSIZE];
int top; //用于栈顶指针
}Stack;
- 进栈操作push
/* 插入元素e为新的栈顶元素 */
Stack Push(Stack *S, SElemType e)
{
if(S->top == MAXSIZE-1) //栈满
return ERROR;
S->top++; //栈顶指针+1
S->data[S->top] = e;
return OK;
}
- 出栈操作pop
/* 若栈不空,则删除S的栈顶元素,用e返回其值,并返回OK,否则返回ERROR */
Stack Pop(Stack *S, SElemType *e)
{
if(S->top == -1){
return ERROR;
}
*e = S->data[S->top]; //将要删除的栈顶元素赋值给e
S->top--; //栈顶指针-1
return OK;
}
- 释放内存(自己写的)
这样写不一定对,要分数组是malloc申请的还是直接声明的(例如:int a[100])
他是malloc申请的数组,销毁的时候,可以直接这样吧,
一般数组栈有一个top指针,,直接置为-1就行了
free(data);
data = NULL;
1.2(链表实现)
- 采用向链表头插元素的方式入栈
#include<stdbool.h>
#include<stdlib.h>
typedef int datatype; //便于修改栈内存储的数据类型
typedef struct stack{
datatype data;
struct stack *next;
}Stack;
Stack *s; //创建栈
void init(){ //初始化栈
s=NULL;
}
//判断栈是否为空
bool Empty()
{
if(s==NULL)
{
return true;
}
else
{
return false;
}
}
//判断栈是否已满了
// void full(Stack *s)
// {
// if(s->top==realsize-1)
// {
// realsize++;
// s->data=(datatype *)realloc(s->data,realsize);
// }
// }
//入栈
void Push(datatype element)
{
Stack *p = (Stack *)malloc(sizeof(Stack));
p->data=element;
p->next=s; //头插
s=p;
}
//出栈
void Pop()
{
if(!Empty(s))
{
s=s->next;
}
else
{
printf("栈空\n");
}
}
//取栈顶元素
datatype Top()
{
if(!Empty(s))
{
return s->data;
}
else
{
printf("栈空\n");
}
}
//销毁栈
void Destroy()
{
free(s);//应该销毁每一个元素 (元素全部出栈了才能只free头结点,不然的话要一个个先把元素都释放)
s=NULL;
// free(s.data); //容易导致失败
}
上述直接free头结点的方式销毁栈好像可行。
1.3 (两栈共享空间)
参考《大话数据结构》
-
问题引出
为了解决栈空间不够用的问题,一般情况下数组栈都是事先定好数组空间,万一不够用了就需要编程手段来扩展数组容量,非常麻烦。 -
解决办法
用一个数组来存储两个栈
-
代码实现
/*两栈共享空间结构*/
typedef struct{
SElemType data[MAXSIZE];
int top1; //栈1栈顶指针
int top2; //栈2栈顶指针
}Stack;
/*插入元素e为新的栈顶元素*/
Status Push (Stack *S, SElemType e, int StackNumber){
if(S->top1 + 1 == S->top2) //栈已满,不能再push新元素
return error;
if(StackNumber == 1)
S->data[++S->top1] = e; //若栈1则给栈1入栈
else if(StackNumber == 2)
S->data[--S->top2] = e;
return OK;
}
/*选择栈完成元素出栈*/
Status Pop (Stack *S, SElemType *e, int StackNumber){
if(StackNumber == 1){
if(S->top1 == -1)
return ERROR; //栈1空
*e = S->data[S->top1--];//栈1出栈
}
else if(StackNumber == 2){
if(S->top2 == MAXSIZE)
return ERROR; //栈2空
*e = S->data[S->top2++];//栈2出栈
}
return OK;
}
4. 队列(Queue)
1.(链式结构)
一串结构体相连形成队列本质上还是用链表
C语言代码实现 参考
Queue.h 文件:
1、定义结构体结构
//创建队列结构
typedef int QDataType; //方便后续更改存储数据类型,本文以int为例
//创建队列节点
typedef struct QueueNode
{
QDataType data; //存储数据
struct QueueNode* next; //记录下一个节点
}QNode;
//保存队头和队尾
2、队列头尾指针的结构体
typedef struct Queue
{
QNode* head; //头指针
QNode* tail; //尾指针
}Queue;
//对于求树的最大深度问题
3、数据入队列
/*
* Data queuing
*/
void enqueue(QUEUE_TYPE value) {
QueueNode *new_node;
new_node = (QueueNode *)malloc(sizeof(QueueNode));
if (new_node == NULL)
perror("malloc fail\n");
new_node->data = value;
if (head == NULL) {
head = new_node;
tail = new_node;
} else {
tail->next = new_node;
tail = new_node;
}
}
4、数据出队列
/*
* Data out of queue
*/
void dequeue(void) {
assert(!is_empty());
QueueNode *front_node;
front_node = head;
head = front_node->next;
free(front_node);
if (head == NULL)
tail = NULL;
}
2. 两个栈实现队列
1.栈的特点是先进后出,队列的特点是先进先出,这里我们可以使用两个栈(栈A和栈B)模拟队列;
栈B为辅助栈
2. 弹出队首的数据,实际就是删掉栈底的数据,直接删除不了,可以利用另一个辅助栈,将当前栈中数据全部依次弹出,并push到辅助栈中,这时当前栈的栈底元素就变成了辅助栈的栈顶元素,直接pop掉辅助栈的栈顶元素就行。
3. 队列从队首到队尾的元素始终是辅助栈栈顶到栈底+当前栈栈底到栈顶。辅助栈栈顶对应着队列队首,当前栈栈底对应队列队尾。这就说明当辅助栈不为空时,队列的pop,可以直接对辅助栈pop。当辅助栈为空时,就要将当前栈搬运到辅助栈再弹出。而入栈可以直接push当当前栈。
- C语言实现
typedef struct{
int* data;
int top;
}MyStack;
typedef struct{
MyStack* stackPush; //输入栈
MyStack* stackPop; //输出栈(辅助栈)
}MyQueue;
#define maxQueueNum 100 //队列空间
MyQueue* myQueueCreate(){
MyQueue* queue = (MyQueue*)malloc(sizeof(MyQueue));
queue->stackPush = (MyStack*)malloc(sizeof(MyStack));
queue->stackPop = (MyStack*)malloc(sizeof(MyStack));
queue->stackPush->data = (int*)malloc(sizeof(int) * maxQueueNum);
queue->stackPush->top = -1;
queue->stackPop->data = (int*)malloc(sizeof(int) * maxQueueNum);
queue->stackPop->top = -1;
return queue;
}
//入队
void myQueuePush(MyQueue* obj){
if((obj->stackPush->top + obj->stackPop->top) == maxStackNum - 2) //队列空间满
return ;
obj->stackPush->data[++obj->stackPush->top] = x;
}
//出队
int myQueuePop(MyQueue* obj){
if(obj->stackPop->top != -1){ //辅助栈不为空,直接弹出
return obj->stackPop->data[obj->stackPop->top--];
}
while(obj->stackPush->top != -1){ //辅助栈为空,将当前栈依次弹出并压到辅助栈中
obj->stackPop->data[++obj->stackPop->top] = obj->stackPush->data[obj->stackPush->top--];
}
if(obj->stackPop->top == -1){ //辅助栈仍为空,说明对应队列为空,无值返回
return -1;
}
else
{
return obj->stackPop->data[obj->stckPop->top--];
}
}
//取队首值
int myQueuePeek(MyQueue* obj){
if((obj->stackPush->top == -1) && (obj->stackPop->top == -1))//队列为空
return -1;
if(obj->stackPop->top != -1){
return obj->stackPop->data[obj->stackPop->top];
}
else
{
while(obj->stackPush->top != -1){
obj->stackPop->data[++obj->stackPop->top] = obj->stackPush->data[obj->stackPush->top--];
}
return obj->stackPop->data[obj->stackPop->top];
}
}
//判断队列是否为空
bool myQueueEmpty(MyQueue* obj){
if((obj->stackPush->top == -1) && (obj->stackPop->top == -1))
return true;
else
return false;
}
void myQueueFree(MyQueue* obj) {
free(obj->stackPush->data);
free(obj->stackPop->data);
free(obj->stackPush);
free(obj->stackPop);
free(obj);
}
- C++实现
class CQueue {
private:
stack<int> inStack, outStack; //创建了两个栈
void in2out() {
while (!inStack.empty()) {
outStack.push(inStack.top());
inStack.pop();
}
}
public:
CQueue() {}
void appendTail(int value) {
inStack.push(value);
}
int deleteHead() {
if (outStack.empty()) {
if (inStack.empty()) {
return -1;
}
in2out();
}
int value = outStack.top();
outStack.pop();
return value;
}
};
5.DFS/BFS
深度优先级搜索/广度优先搜索
(DFS,Depth-First Search)
(BFS,Breath-First Search)
DFS:在树中,用递归的方式,寻找一个深度最大的路径
BFS:在树中,将树一层一层拼接为一个队列,看有几层
深度优先搜索相当于对搜索树进行前序遍历,而广度优先搜索则相当于层序遍历。
6. C言语哈希表(uthash)使用
参考CSDN
哈希函数形象比喻:
关键字(key)—— 哈希函数(f()) —— 哈希值
例如要在字典上查“按”这个字
按就是关键字(key),f()就是字典索引,也就是哈希函数,查到的页码4就是哈希值。
由于C语言本身不存在哈希,但是当需要使用哈希表的时候自己构建哈希会异常复杂。因此,我们可以调用开源的第三方头文件,这只是一个头文件:uthash.h。我们需要做的就是将头文件复制到您的项目中,然后:#include “uthash.h”。
uthash还包括三个额外的头文件,主要提供链表,动态数组和字符串。utlist.h为C结构提供了链接列表宏。utarray.h使用宏实现动态数组。utstring.h实现基本的动态字符串。
github下载链接:https://github.com/troydhanson/uthash
一、 uthash的使用
以构建一个姓名和学号的哈希表为例
- 第一步:包含头文件
#include "uthash.h"
- 第二步:自定义数据结构。每个结构代表一个键-值对,并且,每个结构中要有一个UT_hash_handle成员。
struct my_struct {
int id; /* key */
char name[10];
UT_hash_handle hh; /* hh是内部使用的hash处理句柄,不需要为该句柄变量赋值,但必须在该结构体中定义该变量。 */
};
- 第三步:定义hash表指针。这个指针为前面自定义数据结构的指针,并初始化为NULL。
struct my_struct *users = NULL; /* important! initialize to NULL */
- 第四步:进行一般的操作。
增加:
int add_user(int user_id, char *name) {
struct my_struct *s;
s = malloc(sizeof(struct my_struct));
s->id = user_id;
strcpy(s->name, name);
HASH_ADD_INT( users, id, s ); /* id: name of key field */
}
- HASH_ADD_INT函数中,第一个参数users是哈希表,第二个参数id是键字段的名称。最后一个参数s是指向要添加的结构的指针。
查找:
struct my_struct *find_user(int user_id) {
struct my_struct *s;
HASH_FIND_INT( users, &user_id, s ); /* s: output pointer */
return s;
}
- 在上述代码中,第一个参数users是哈希表,第二个参数是user_id的地址(一定要传递地址)。最后s是输出变量。当可以在哈希表中找到相应键值时,s返回给定键的结构,当找不到时s返回NULL。
删除:
void delete_user(struct my_struct *user) {
HASH_DEL( users, user); /* user: pointer to deletee */
free(user); /* optional; it’s up to you! */
}
计数:
unsigned int num_users;
num_users = HASH_COUNT(users);
printf("there are %u users/n", num_users);
迭代:
void print_users() {
struct my_struct *s;
for(s=users; s != NULL; s=s->hh.next) {
printf("user id %d: name %s/n", s->id, s->name
}
}
排序:
int name_sort(struct my_struct *a, struct my_struct *b) {
return strcmp(a->name,b->name);
}
int id_sort(struct my_struct *a, struct my_struct *b) {
return (a->id - b->id);
}
void sort_by_name() {
HASH_SORT(users, name_sort);
}
void sort_by_id() {
HASH_SORT(users, id_sort);
}
要注意,在uthash中,并不会对其所储存的值进行移动或者复制,也不会进行内存的释放。
3、刷题实践
输入输出——————————————
1. 利用scanf输入两行数字:
- scanf输入不定长数组:
int a[100]={0};
int length=0;
do{
scanf("%d",&a[length++]);
}while(getchar()!='\n'); //scanf输入不定长数组
for(int i=0;i<length;i++)
{
printf("%d ",a[i]);
}
printf("\n");
printf("%d \n",length);
return 0;
}
2. 多行输入
题目描述:
求a + b
输入描述:
输入包括两个正整数a,b(1 <= a, b <= 1000),输入数据包括多组(多组a和b)。
输出描述:
输出a+b的结果
#include <stdio.h>
int main(){
int a,b;
while(scanf("%d %d", &a, &b) != EOF){
printf("%d\n", a+b);
}
return 0;
}
3. 二维数组输入多个字符串
- 利用scanf输入字符串:
char a[10] = {0};
scanf("%s",a);
- 题目描述
对输入的字符串进行排序后输出
输入描述:
输入有两行,第一行n
第二行是n个字符串,字符串之间用空格隔开
输出描述:
输出一行排序后的字符串,空格隔开,无结尾空格
int n, a, b;
scanf("%d", &n);
char arr[n][100];
for(int i = 0; i < n; i++){
scanf("%s", arr[i]);
}
qsort(arr, n, sizeof(arr[0]), strcmp); //用到了字符串比较函数作为快排函数
for(int i = 0; i < n-1; i++){
printf("%s ", arr[i]);
}
printf("%s", arr[n-1]);
return 0;
}
- 二维字符串数组的问题
char a[2][10];
a[0] = "shhajsa";
这是错误的
原因:
(1)数组不能直接给数组赋值
(2)指针不能直接给数组赋值
//前提条件
char a[] = {'a','b','c'};
char b[3];
char c[3] = a; //错误---》数组不能直接给数组赋值
char d[3] = p; //错误---》指针不能直接给数组赋值
char *p = a; //正确赋值,数组名为数组首元素地址,因此可使用指针保存该地址(指针只能保存地址)
strcpy(b,a); //正确,使用标准库函数可实现字符串拷贝
char **p1 = &p; //正确,二级指针可接收一级指针的地址
正确做法:
char a[2][10] = {"shhajsa","hhahjha"};
4. 二维数组输入多行数字
- 输入样例
2 5 //行数和列数
1 4 6 23 1
1 -5 2 4 6 - 代码实现
int main(void)
{
int a[101][101],m,n,i,j;
scanf("%d%d",&m,&n);
for(i=0;i<m;i++){
for(j=0;j<n;j++){
scanf("%d",&a[i][j]);
}
}
//输出
for(i=0;i<m;i++){
for(j=0;j<n;j++){
printf("%d ",a[i][j]);
}
printf("\n");
}
return 0;
}
数据结构——————————————
1. 链表
1.链表结构体
struct ListNode{
int val; //数据域
struct ListNode *next; //指针域
}
- F和Q分别是指向单链表两个元素的指针,那么,F所指元素是Q所指元素后继的条件是(Q->next == F)
1.力扣链表题
1.剑指 Offer 06. 从尾到头打印链表
输入一个链表的头节点,从尾到头反过来返回每个节点的值(用数组返回)。
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* struct ListNode *next;
* };
*/
/**
* Note: The returned array must be malloced, assume caller calls free().
*/
int* reversePrint(struct ListNode* head, int* returnSize){
struct ListNode* p = head;
int i = 0;
static int arry[10000];
//if(head == NULL) return NULL;
while(p != NULL)
{
i++;
p = p -> next;
}
*returnSize = i;
p = head;
while(p != NULL)
{
arry[--i] = p -> val;
p = p -> next;
}
return arry;
}
- 笔记
Line 207: Char 3: runtime error: load of null pointer of type ‘int’ [Serializer.c]
得出这是因为你在函数里设置的是局部变量数组,当退出该函数,该变量数组也会随之销毁。当指针再回来找该变量是找不到的,你return也return了个寂寞
局部变量不能return,用static关键字修饰一下就好了。
2.剑指 Offer 18. 删除链表的节点
给定单向链表的头指针和一个要删除的节点的值,定义一个函数删除该节点。
返回删除后的链表的头节点。
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* struct ListNode *next;
* };
*/
struct ListNode* deleteNode(struct ListNode* head, int val){
//struct ListNode* h = (struct ListNode*)malloc(sizeof(struct ListNode));
//h->next = head; //这是给无头结点链表创建有个头结点的规范做法,本题用不到。
struct ListNode* p = head;
if(p->val == val)
{
return head->next;
}
while(p != NULL)
{
if(p->next->val == val) break;
p = p->next;
}
p->next = p->next->next;
return head;
}
- 心得
- 题目中的链表一般是不带头节点的链表
- 也就是head节点直接具有自己的val和next
- 如果想要自己创建一个头结点(只有next,val为空)
- 则需要用malloc(因为要创建的头结点是一个实体,需要先给它分配空间,不像p只是一个结构体指针,只需要指向链表的各个节点就好了)
- 方法二、有头结点
struct ListNode* deleteNode(struct ListNode* head, int val){
struct ListNode* cur = (struct ListNode*)malloc(sizeof(struct ListNode));
struct ListNode* ans = cur;//以便返回更改后的链表
cur->next = head;
while(cur->next != NULL && cur->next->val != val){
cur = cur->next;
}
if(cur->next != NULL){//找到目标节点
cur->next = cur->next->next;//执行删除操作
return ans->next;//返回新链表
}
return NULL;
}
3.剑指 Offer 22. 链表中倒数第k个节点
输入一个链表,输出该链表中倒数第k个节点。为了符合大多数人的习惯,本题从1开始计数,即链表的尾节点是倒数第1个节点。
例如,一个链表有 6 个节点,从头节点开始,它们的值依次是 1、2、3、4、5、6。这个链表的倒数第 3 个节点是值为 4 的节点。
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* struct ListNode *next;
* };
*/
struct ListNode* getKthFromEnd(struct ListNode* head, int k){
//1、简单做法
/*
struct ListNode* p = head; //遍历指针
struct ListNode* n = head; //返回指针
int i = 1;
if(head == NULL) return NULL;
while(p->next != NULL) //计数循环
{
p = p->next;
i++;
}
for(int j=0; j < (i-k); j++) //找出倒数第k个节点
{
n = n->next;
}
return n;
*/
//2、优雅方法(快慢指针)
struct ListNode*p = head,*slow_p = head;
while(--k) p = p->next;
while(p->next != NULL)
{
p = p->next;
slow_p = slow_p->next;
}
return slow_p;
}
4.剑指 Offer 24. 反转链表
定义一个函数,输入一个链表的头节点,反转该链表并输出反转后链表的头节点。
示例:
输入: 1->2->3->4->5->NULL
输出: 5->4->3->2->1->NULL
限制:0 <= 节点个数 <= 5000
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* struct ListNode *next;
* };
*/
struct ListNode* reverseList(struct ListNode* head){
//简单方法(暴力解法)
/*
struct ListNode* p = head;
struct ListNode* n = head;
int a[5000];
int i = 0;
while(p != NULL) //将链表各个节点放入数组
{
a[i++] = p->val;
if(p->next != NULL)
p = p->next;
else break;
}
for(int j = 1; j <= i; j++)
{
n->val = a[i-j];
n = n->next;
}
return head;
*/
//优雅做法(三指针) 回头再啃一遍
struct ListNode* prev = NULL;
struct ListNode* curr = head;
while (curr) {
struct ListNode* next = curr->next;
curr->next = prev;
prev = curr;
curr = next;
}
return prev;
}
- 感悟:
- 指针 = 指针
左值是指针,右值是地址,左边的指针指向右边指针所指的地址。 - curr->next = prev;
左值是指针,右值是地址,左边的指针指向右边指针所指的地址。
3.经验总结
2. 树
树的各种品种,参考CSDN
1.树的结构体
struct TreeNode {
int val; //数据域
struct TreeNode *left; //左子树 (运用了结构体嵌套)
struct TreeNode *right; //右子树 (运用了结构体嵌套)
};
2.二叉树
1. 题目引入
- 已知二叉树后序遍历序列为dabec,中序遍历序列为debac,它的前序遍历序列为
答案: cedba
2. 前/中/后/层 序遍历
- 递归实现前/中/后/层 序遍历
参考CSDN
- 前序遍历:根节点 | 左子树 | 右子树
- 中序遍历:左子树 | 根节点 | 右子树
- 后序遍历:左子树 | 右子树 | 根节点
- 后序遍历:一层一层遍历
-
前序遍历: 1 2 4 5 3 6 7
-
中序遍历: 4 2 5 1 6 3 7
-
后序遍历: 4 5 2 6 7 3 1
-
层序遍历: 1 2 3 4 5 6 7
3. 树的前、中、后 / 层序遍历代码实现
1. 前、中、后序遍历中序
- 输出一个二叉树
- 输入:二叉树
- 输出:二叉树的中序数组和数组元素个数;
#include <stdio.h>
#include<stdlib.h>
//int * a = (int*)malloc(sizeof(int)*1000);
int a[1000] = {0}; //在C标准中全局变量要初始化
static int i = 0; //全局变量最好拿static修饰,防止和其他文件变量重名
struct tree {
int val;
struct tree* left;
struct tree* right;
};
struct tree* digui(struct tree* root){ //递归遍历函数
if(root != NULL){
digui(root->left);
a[i++] = root->val; //这一行是关键,前中后序分别放在前中后
digui(root->right);
}
return root;
}
int* zhongxu(struct tree* root, int* numSize){ //整合函数
digui(root);
*numSize = i;
return a;
} //到这里答题就算完成了——————————————————————————————————————————————————————————————————
int main(void){ //创建一个小树,测试输出
printf("进入main\n");
struct ListNode* root1 = (struct ListNode*)malloc(sizeof(struct ListNode));
root1->val = 5;
root1->left = NULL;
root1->right = NULL;
int *n = (int*)malloc(sizeof(int)); //声明指针就要分配空间,不要存在空指针。
int *b = zhongxu(root1,n);
for(int j = 0; j<i; j++)
printf("该树的中序遍历为:{ %d }\n",b[j]);
printf("numSize = %d\n",*n);
}
2. 层序遍历
- 层序遍历
//先记录当前节点的值,然后再遍历左孩子,直到当前节点没有左孩子则遍历其右孩子
//若右孩子也没有则回溯到上一个节点遍历其右孩子,直至遍历完树
//时间复杂度O(N), 空间复杂度O(N)
原理:
一个结构体数组形成的队列动态放置树的节点的层序
一个结构体指针不断的往这个队列中放入节点
一个数组不断从队列中取节点的值
int* levelOrder(struct TreeNode* root, int* returnSize){
if(root == NULL){
(*returnSize)= 0;
return root;
}
struct TreeNode* queue[1001];
int front = 0, rear = 0, i = 0;
struct TreeNode* p;
int* array = (int*)malloc(sizeof(int)*1001);
queue[rear++] = root;
while(front < rear){
p = queue[front++];
array[i++] = p->val;
if(p->left)
queue[rear++] = p->left;
if(p->right)
queue[rear++] = p->right;
}
*returnSize = i;
return array;
}
3. 堆/栈
算法—————————————————
4、排序
4.1、C库函数
0.qsort():快速排序
参考CSDN
- 函数原型
#iclude<stdlib.h>
void qsort(void*, size_t, size_t, int(*)(const void*, const void*))
第一个参数为待排序数组首地址。
可直接输入待排序数组名,或是指向数组的指针。
第二个参数为数组长度。
size_t是标准C库中定义的,应为unsigned int,在64位系统中为 long unsigned int。
可以直接输入待排序的数组的长度。
第三个参数为数组元素所占字节。
可直接用sizeof(a[0])计算字数组单个元素的节数。
第四个参数为所调用函数的指针,函数名即是函数的指针,可直接写函数名,调用函数用来确定排序的方式。
- 第四个参数(传一个函数)
先以整型递增排序为例
int inc(const void*a, const void*b){
return *(int*)a - *(int*)b;
}
- int inc 表示函数返回一个int值。inc为函数名 ,表示递增排序(increase),也可以自己命名。
- ( const void * a, const void * b)将两个要对比的元素的地址传入函数。 (加const表示无法改变指针指向的值)
- return * ( int * )a - * ( int * ) b ,返回一个整型数,表示两个元素对比的结果。如果a大于b,则返回正数。a小于b,则返回负数。如果a等于b,则返回零。(int *)表示将地址强制类型转换成整形地址类型,可根据排序对象选择指针类型的转换。也可以改变算式,例如用 return strlen((char * )a) > strlen((char * )b) ? 1 : -1; 可以返回比较字符串长度的结果,用来根据字符串长度进行排序, 下面有相关的代码。
- 各种数据类型的升序排序函数
- 如果要降序排序,只需将return里的a,b反过来写即可。
1、整型
int inc (const void * a,const void *b)
{
return *(int *)a - *(int *)b;
}
2、double型
int inc (const void * a, const void * b)
{
return *(double *)a > *(double *)b ? 1 : -1;
}
注: 这里两个浮点数相减但要返回一个整型数,如果按上面做法直接减会丢失小数点部分。所以需另加处理,直接判断大小,如果a大于b,则返回1,否则返回-1。
3、字符排序
int inc(const void *a,const void *b)
{
return *(char *)a - *(char *)b;
}
- 力扣例题
剑指 Offer 39. 数组中出现次数超过一半的数字
int increase(const void* a, const void* b){ //直接写(int* a, int* b)也可
return *(int*)a - *(int*)b; //都去掉(int*)也可
}
int majorityElement(int* nums, int numsSize){
qsort(nums, numsSize, sizeof(int), increase); //采用C库函数,快速排序
return nums[numsSize / 2];
}
- 对结构体数组进行排序
以结构体的val进行排序
int cmp(const void *a, const void *b){
return (*((hash*)a)).val - (*((hash*)b)).val; //直接取值用结构体变量也可以进行值的访问
}
调用:
qsort(H, arrSize, sizeof(hash), cmp);
4.2、C语言实现各种排序
0. 各排序算法复杂度
(时间复杂度)
方式: 平均——最坏——最好
插入: n^2 —— n^2——n
希尔: n^1.3—— /—— /
冒泡 :n^2 ——n^2 ——n
快速 :nlogn—— n^2 ——nlogn
选择: n^2—— n^2—— n^2
堆排: nlogn—— nlogn ——nlogn
归并: nlogn ——nlogn ——nlogn
基数: d(n+r) ——d(n+r)—— d(n+r)
其中平均和最坏均为nlogn的有堆排 和 归并
1. 冒泡排序 O(n^2)
循环比较两个元素,较大值(往后移)冒出来,最后移到最后一个,下一次再循环比较,最后移到倒数第二个;循环结束后,数组有序.
- 法1:(冒泡排序优化版)
/*冒泡排序优化版*/
void SelectSort(int arr[],size_t len){
for(int i=0;i < len - 1; i++){
for(int j=i; j<=len - 1; j++){
if(arr[j]<arr[i])
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}
}
- 法2:(正宗冒泡排序)
相邻两个元素比较,按顺序交换位置,直到将最大值移动到最后。
void swap(int* a, int* b){
int temp = *a;
*a = *b;
*b = temp;
}
int SelectSort(int *s, size_t len){
int i = 0, j = len - 1;
for(; j > 0; j--){
while(i < j){
if(s[i] > s[i+1]) swap(&s[i], &s[i+1]);
i++;
}
}
return 0;
}
2. 选择排序 O(n^2)
遍历数组 能够记录 最大值的下标
把最大值 和 最后一个元素 交换
遍历第二遍(不需要最后一个)数组 又能记录最大值的下标
把最大值 和 倒数第二个元素进行交换循环 len-1 次
//正宗选择排序(从小到大排序)
void SelectSort(int R[], int n)
{
int i, j, min, tmp;
for (i = 0; i < n - 1; i++)
{
min = i;
for (j = i; j < n - 1; j++)/*比较完成再交换值*/
{
if (R[j] < R[min])
min = j;
}
if (min != i) //当最小值不在该位置时
{
tmp = R[min];
R[min] = R[i];
R[i] = tmp;
}
}
}
3.快速排序 O(nlogn)
记下左右两端的数序号[left,right],记录一个数比如说 data = arr[left] 序号
然后去left右边 找一个比 arr[left] 小的数放在左边
后又去right左边找一个比 arr[left] 大的数放在右边
循环直到最后
递归调用自己 执行每一个数
void quick(int arr[],int left,int right){
int data = arr[left];
int i = left;
int j = right;
while(i<j){
while(i<j&&arr[j]>data){--j;} //查找左边的小于等于data的数
arr[i] = arr[j]; //放置在左边i
while(i<j&&arr[i]<data){++i;} //查找右边大于等于data的数
arr[j] = arr[i]; //放置在右边j
}
arr[i] = data;
if(i-left>1){ //假如左边的数个数大于1,再次递归循环
quick(arr,left,i-1);
}
if(right-i>1){ //假如右边的数的个数>1,再次递归循环
quick(arr,i+1,right);
}
}
void quickSort(int arr[],size_t len){
quick(arr,0,len-1);
}
4. 堆排序 O(nlogn)
面试题40. 最小的k个数
参考bilibili
参考CSDN
堆排序是指利用堆这种数据结构所设计的一种排序算法。堆是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或大于)它的父节点。
一、数组转换二叉树,从上往下、从左往右对应数组下标,则对于任意结点下标k
- 其父结点下标 =【(k - 1)/ 2】
- 左孩子下标 =【2k + 1】
- 右孩子下标 =【2k + 2】
例如:
- 堆排序步骤:
- 最大堆调整(Heapify)
该步可以用一个函数实现局部的子树的最大堆; - 创建最大堆(CreateHeap)
该步借助树节点元素的变换,调用Heapify使得整个树变成一个最大堆(父节点都比子节点大); - 堆排序(HeapSort)
你可能会疑问既然是最大堆,那直接层序遍历输出不就好了,其实,推排序只是借助树和堆的思想,本质还是拿数组实现,所以没法层序遍历输出,还是得老老实实通过下标交换输出。
- 代码实现
一共四个函数:
元素交换(Swap)
最大堆调整(Heapify)
创建最大堆(CreateHeap)
堆排序(HeapSort)//还不如拿一个数组去接收最大堆的数组的倒序呢(想太少了,最大堆又不是从大到小以此排序的)
#include <stdio.h>
#define size 10
void Swap(int *num, int i, int j)
{
int temp;
temp = num[i];
num[i] = num[j];
num[j] = temp;
}
// 最大堆调整
void Heapify(int *num, int len, int k)
{
if (k < len)
{
int root = k; // 根结点
int lchild = 2*k + 1; // 左孩子结点
int rchild = 2*k + 2; // 右孩子结点
// 查找左右孩子结点中的最大结点
if (lchild < len && num[root] < num[lchild])
{
root = lchild;
}
if (rchild < len && num[root] < num[rchild])
{
root = rchild;
}
// 交换最大结点到根结点
if (root != k)
{
Swap(num, root, k);
// 每次交换都可能影响到对应孩子结点子树的顺序
// 所以需要对交换后的孩子结点子树进行最大堆调整
Heapify(num, len, root);
}
}
}
// 创建最大堆
void CreateHeap(int *num, int len)
{
int i;
// 最后一个结点下标
int last = len - 1;
// 最后一个结点的父结点下标
int parent = (last - 1) / 2;
// 从最后一个结点的父结点到根结点,依次进行最大堆调整
for (i = parent; i >= 0; i--)
{
Heapify(num, len, i);
}
}
// 堆排序
void HeapSort(int *num, int len)
{
// 创建最大堆并进行最大堆调整
CreateHeap(num, len);
printf("最大堆调整!\n");
int i;
// 依次取出根结点(最大值)
for (i = len - 1; i >= 0; i--)
{
// 将最大堆的根结点(最大值)换到最后一个结点
Swap(num, i, 0);
// 交换后二叉树的根结点发生了改变,故还需对根结点做最大堆调整(已交换的末尾结点不参与调整)
// 而此时根结点小于所有父结点,(因而在调整时只需考虑最大孩子的分支即可)?对这句话保留意见
Heapify(num, i, 0);
}
}
int main()
{
int i, num[size] = {8, 4, 3, 1, 6, 9, 5, 7, 2, 0};
HeapSort(num, size);
for (i = 0; i < size; i++)
{
printf("%d ", num[i]);
}
printf("\n");
return 0;
}
4. 直接插入排序 O(n^2)
5. 二叉树 堆排序 O(nlogn)
6. 希尔插入排序 O(nlogn)
7. 归并排序 O(nlogn)
5、贪心算法
- 例1见蔚来笔试准备篇
给你一个 m 行 n 列的二维网格 grid 和一个整数 k。你需要将 grid 迁移 k 次。
每次「迁移」操作将会引发下述活动:
位于 grid[i][j] 的元素将会移动到 grid[i][j + 1]。
位于 grid[i][n - 1] 的元素将会移动到 grid[i + 1][0]。
位于 grid[m - 1][n - 1] 的元素将会移动到 grid[0][0]。
请你返回 k 次迁移操作后最终得到的 二维网格。
示例 1:
int** shiftGrid(int** grid, int gridSize, int* gridColSize, int k, int* returnSize, int** returnColumnSizes){
int m = gridSize, n = gridColSize[0];
*returnColumnSizes = (int*)malloc(sizeof(int)*m);//A:为这个二维数组对应的一维数组申请m行
int **ret = (int**)malloc(sizeof(int*)*m); //1.为二维数组申请m个一维数组空间
for(int i = 0; i < m; i++){
ret[i] = (int*)malloc(sizeof(int)*n); //2.为每个一维数组申请空间
(*returnColumnSizes)[i] = n; //B:为这个一维数组各个元素赋值?
}
for(int i = 0; i < m; i++){
for(int j = 0; j < n; j++){
int index = (i*n+j+k)%(m*n);
ret[index/n][index%n] = grid[i][j];
}
}
*returnSize = m;
return ret;
}
6、动态规划
动态规划中每一个状态一定是由上一个状态推导出来的
-
动态规划问题五步曲
1) 确定dp数组(dp table)以及下标的含义
2)确定递推公式
3)dp数组如何初始化
4)确定遍历顺序
5)举例推导dp数组 -
青蛙跳台阶问题
假设有N级台阶,一次跳两节或者一节
int numWays(int n){
int arr[n+2];
arr[0] = 1;
arr[1] = 1;
if(n > 1){
for(int i = 2; i <= n; i++){
//arr[i]代表取余结果;
arr[i] = (arr[i-1] + arr[i-2])%1000000007; //除以同一个数,和的余数=两个加数的余数之和的余数
}
}
return arr[n];
}
7、dfs / bfs
(DFS,Depth-First Search)
(BFS,Breath-First Search)
深度优先级搜索/广度优先搜索
1. 深度优先级搜索
- 剑指 Offer 13. 机器人的运动范围
- 题目描述:
地上有一个m行n列的方格,从坐标 [0,0] 到坐标 [m-1,n-1] 。一个机器人从坐标 [0, 0] 的格子开始移动,它每次可以向左、右、上、下移动一格(不能移动到方格外),也不能进入行坐标和列坐标的数位之和大于k的格子。例如,当k为18时,机器人能够进入方格 [35, 37] ,因为3+5+3+7=18。但它不能进入方格 [35, 38],因为3+5+3+8=19。请问该机器人能够到达多少个格子?
//官方解法,dfs深度优先搜索
// 计算各个位数之和
int digitSum(int num){
int sum = 0;
while (num > 0){
sum += num % 10;
num /= 10;
}
return sum;
}
// x,y 当前位置// m,n 当前矩阵长和宽// visited 访问标记// k 目标值
int dfsTraversal(int x, int y, int m, int n, bool **visited, int k)
{
if (x < 0 || y < 0 || x >= m || y >= n || digitSum(x) + digitSum(y) > k || visited[x][y] == true)
return 0;
/* 标记当前节点已到达 */
visited[x][y] = true;
return 1 + dfsTraversal(x, y + 1, m, n, visited, k) + dfsTraversal(x + 1, y, m, n, visited, k);
}
int movingCount(int m, int n, int k){
bool **visited = (bool **)malloc(sizeof(bool *) * m);
for (int i = 0; i < m; i++)
{
visited[i] = (bool *)calloc(n, sizeof(bool));
}
return dfsTraversal(0, 0, m, n, visited, k);
}
8、回溯
给定两个整数 n 和 k,返回范围 [1, n] 中所有可能的 k 个数的组合。
你可以按 任何顺序 返回答案。
- 递归+for循环
//代码随想录学习
int* path;
int pathTop;
int** ans;
int ansTop;
void backtracking(int n, int k, int startIndex){
//当path中元素个数为k个时,我们需要将path数组放入ans二维数组中
if(pathTop == k){
//path数组为我们动态申请,若直接将其地址放入二维数组,path数组中的值会随着我们回溯而逐渐变化
//因此创建新的数组存储path中的值
int* temp = (int*)malloc(sizeof(int) * k);
for(int i = 0; i < k; i++){
temp[i] = path[i];
}
ans[ansTop++] = temp;
return;
}
//for循环来了!
//剪枝操作 for(j = startIndex; j <= n- (k - pathTop) + 1;j++)
for(int j = startIndex; j <= n; j++){
//将当前结点放入path数组
path[pathTop++] = j;
backtracking(n, k, j + 1);
pathTop--;
}
}
int** combine(int n, int k, int* returnSize, int** returnColumnSizes){
path = (int*)malloc(sizeof(int) * k);
ans = (int**)malloc(sizeof(int*) * 10000);
pathTop = ansTop = 0;
//回溯算法
backtracking(n, k, 1);
*returnSize = ansTop; //返回的二维数组的行数
*returnColumnSizes = (int*)malloc(sizeof(int) * (*returnSize)); //返回的二维数组每行的列数
for(int i = 0; i < *returnSize; i++){
(*returnColumnSizes)[i] = k;
}
return ans;
}
9、查找
- 二分法查找
用于已排序好的数组
/*
非递归二分法查找
srcArray:输入的数组
nSize:数组的元素个数
key:要查找的数
*/
int SearchBin(int *srcArray, int nSize, int key){
if(!srcArray || nSize == 0) return -1;
int l = 0;
int r = nSize - 1;
int mid = 0;
while(l <= r){
mid = (l + r)/2;
if(srcArray[mid] > key) r = mid;
else if(srcArray[mid] < key) l = mid;
else return mid;
}
return -1;
}
时间/空间复杂度
算法的真理
- 流程控制语句(for、while)、条件判断语句(if、else)、递归 的排列组合;
何时可以用递归?
1.问题的解可以分为几个子问题的解。何为子问题?就是数据规模更小的问题。
2.问题与子问题,出来数据规模不同,求解思路完全一样;
3.存在递归终止条件;
20世纪10个最伟大的算法
参考CSDN
由Computer in Science &Enigeering和IEEE Computer Society联合评选出来的20世纪10个最伟大的算法:
1、蒙特卡罗算法。1946: John von Neumann, Stan Ulam, and Nick Metropolis
2、单纯形方法。1947: George Dantzig,学过运筹学的人都知道:)
3、Krylov 子空间迭代算法。1950: Magnus Hestenes, Eduard Stiefel, and Cornelius Lanczos。Krylov subspace:span{S,A*S,A2*S,…,A(k-1)*S}.
4、矩阵分解算法。1951: Alston Householder。
5、Fotran 最优化编译器。1957: John Backus。
6、QR算法。1959–61: J.G.F. Francis
7、快速排序算法 1962: Tony Hoare。你能在10分钟内写出个完全正确的版本么:)
8、FFT算法。1965: James Cooley
9、整数关系确定算法(Integer Relation Detecting Algorithms)。1977: Helaman Ferguson and Rodney Forcade。
10、快速多极算法(Fast Multipole Algorithms )。1987: Leslie Greengard and Vladimir Rokhlin。
机器学习十大算法
参考:知乎