笔者目前大四即将毕业,回顾大学四年,计算机的核心课程有太多太多,例如:计组、计网、程序设计、数学还有数据结构与算法这门课程,该篇博客就聊聊笔者对于数据结构与算法这门课的思考与学习历程吧!
学数据结构与算法的初衷也包括学任何课程的初衷其实都是为了实际应用而准备的理论基础,不妨我们可以思考,数据结构在我们的实际应用中到底有哪些地方会用到:
一句话总结:“生活中处处都有数据结构”。
与其说它是“有限操作的集合”,我更愿意把它理解成“解决实际问题的方法”,算法的特性包含如下几点:
不管是期末、面试、考研中,数据结构的分类、按照什么结构分类,都是常见的问题。主要从逻辑和存储两个角度进行分类。
从逻辑结构角度
从内存存储角度
ps:篇幅原因就不对顺序存储多做阐述,毕竟我认为链式存储才是核心!
链表其实是整个数据结构的核心,后面的树、图都与链表有关。关于链表,笔者主要聊几个核心的点以及常见的算法。
答:因为在表尾插入的操作,只需要设尾指针,使最后一个结点的next指针指向新结点即可,时间复杂度是O(1)。而假若想在尾部删除表尾元素必须知道表尾元素的前一个元素,这就导致必须从头开始遍历单链表时间复杂度是O(n)。
/*
* 作者:xulinjie
* 描述:单链表逆置
* 思路:将头结点摘下,依次从第一个结点放到头结点之后(头插)
*/
#include
typedef int Elemtype;
typedef struct LNode{
Elemtype data;
struct LNode *next;
}LNode,*LinkList;
//链表逆置
LinkList reverse(LinkList L){
LNode *p; //p为工作指针
LNode *r; //r始终为p的后继,防止断链
p = L->next; //从第一个元素结点开始
L->next = NULL; //先将头结点L的next域置为NULL
while (p!=NULL){ //依次将元素结点摘下
r = p->next; //暂存p的后继
p->next = L->next; //将p结点插入到头结点之后(头插思想)
L->next = p;
p = r;
}
return L;
}
其实理解栈与队列的数据结构特点有一个不恰当但很形象的比喻(中文博大精深-_-):
栈:吃了就吐(后进先出)
队列:吃了就拉(先进先出)
首先不管是栈还是队列,必须明白,它们是线性表,只是加了限制的线性表。所以栈是限定仅在表尾(栈顶)进行插入和删除的线性表。栈其实在许多场景下都有应用,比如:递归就是基于栈的思想(斐波那契数列)、括号匹配等等。写一个括号匹配伪码,供参考和理解
/*
* 作者:xulinjie
* 描述:括号匹配(表达式由字符表示,其中可以包括3种括号:圆括号、方括号、花括号,判定给定表达式是否正确)
* 思想:若是正括号,则入栈;若是反括号,栈空或与栈顶元素不匹配,则匹配失败 ||||| 当数组处理完后,若栈空,则匹配成功,否则失败。
*/
#include
typedef char ElemType;
typedef struct
{
ElemType *base; //栈底指针
ElemType *top; //栈顶指针
int maxsize; //当前可使用最大容量
}sqStack;
char Pop(sqStack S); //出栈
char Push(sqStack S, char a); //入栈
int StackEmpty(sqStack S); //判栈空
void InitStack(sqStack S); //初始化栈
int matching(char *b,int n){
//定义、初始化栈
sqStack S;
InitStack(S);
//括号匹配
//{[()]}是正确格式
for (int i = 0; i < n; ++i) {
switch(b[i]){
case ')':
if (Pop(S)!='('||StackEmpty(S)){
return 0;
}
break;
case ']':
if (Pop(S)!='['||StackEmpty(S)){
return 0;
}
break;
case '}':
if (Pop(S)!='{'||StackEmpty(S)){
return 0;
}
break;
case '('||'['||'{':
Push(S,b[i]);
break;
}
}
return StackEmpty(S);
}
队列是在队尾插入、队头删除的特殊线性表,队列也有很多数据结构比如:循环队列、双端队列、优先队列。
下面写几个考试中常考的队列基本算法伪码
/*
* 作者:xulinjie
* 描述:反向循环队列
* 思想:对尾删除、队头插入(其实就是将原来的队头队尾互换,加变成减)
*/
#include
/**
* 队列存储结构
*/
#define maxsize 10
typedef int QElemType;
typedef struct{
QElemType *data;
int front;
int rear;
}SqQueue;
void InitQueue(SqQueue Q){} //初始化队列
void EnQueue(SqQueue Q){} //入队
void DeQueue(SqQueue Q){} //出队
int IsEmptyQueue(){} //判队空
/**
* 入队
* @param Q
* @param x
* @return
*/
int Enqueue(SqQueue Q,int x){
if(Q.rear == (Q.front-1)%maxsize){ //队满
return 0;
}
Q.data[Q.front] = x; //队头插入
Q.front = (Q.front - 1)%maxsize;
return 1;
}
/**
* 出队
* @param Q
* @param x
* @return
*/
int Dequeue(SqQueue Q,int x){
if(Q.rear == Q.front){ //队空
return 0;
}
x = Q.data[Q.rear];
Q.rear = (Q.rear-1)%maxsize;
return x;
}
/*
* 作者:xulinjie
* 描述:队列逆置
* 思想:1、全部元素出队,进栈。2、全部元素出栈,进队
*/
#include
/**
* 队列存储结构
*/
typedef int QElemType;
typedef struct{
QElemType *data;
int front;
int rear;
}SqQueue;
void EnQueue(SqQueue Q,int x){} //入队
int DeQueue(SqQueue Q){} //出队
int IsEmptyQueue(){} //判队空
/**
* 栈存储结构
*/
typedef char ElemType;
typedef struct
{
ElemType *base; //栈底指针
ElemType *top; //栈顶指针
int maxsize; //当前可使用最大容量
}sqStack;
char Pop(sqStack S); //出栈
char Push(sqStack S, int a); //入栈
int StackEmpty(sqStack S); //判栈空
void reverse(sqStack S,SqQueue Q){
int x;
while (!IsEmptyQueue(Q)){
x = DeQueue(Q); //队列元素依次出队
Push(S,x); //依次入栈
}
while (!StackEmpty(S)){
x = Pop(S); //依次出栈
EnQueue(Q,x); //依次入队
}
}
/*
* 作者:xulinjie
* 描述:层次遍历二叉树
* 思想:利于队列的思想:借助队列,先将二叉树根结点入队,然后出队输出,并判断是否有该结点的左子树,若有则入队;同理判断右子树,若有则入队,再循环出队
*/
#include
/**
* 二叉树存储结构
*/
typedef int TElemType;
typedef struct BiTNode{
TElemType data;
struct BiTNode rchild;
struct BiTNode lchild;
}BiTNode,BiTree;
/**
* 队列存储结构
*/
typedef int QElemType;
typedef struct{
QElemType *data;
int front;
int rear;
}SqQueue;
void EnQueue(SqQueue Q,BiTree T){} //入队
BiTree DeQueue(SqQueue Q){} //出队
int EmptyQueue(){} //判队空
void InitQueue(SqQueue Q){} //初始化队列
void visit(BiTree T){} //打印结点的值
/***
* 层次遍历二叉树
* @param T
*/
void LevelOrder(BiTree T){
BiTree p; //工作树 p
SqQueue Q; //创建队列Q、并初始化
InitQueue(Q);
EnQueue(Q,T);//根结点入队
while (!EmptyQueue(Q)){
p = DeQueue(Q);
visit(p);
if (p.lchild!=NULL){ //左子树不空,则左子树入队
EnQueue(Q,p.lchild);
}
if (p.rchild!=NULL){ //右子树不空,则右子树入队
EnQueue(Q,p.rchild);
}
}
}
/*
* 作者:xulinjie
* 描述:"tag"法循环队列
* 思想:与原来空出一个存储空间的循环队列区别在于判满条件不再用rear+1,不用多耗一个空间,用tag标志来实现
*/
#include
/**
* 队列存储结构
*/
#define maxsize 10
typedef int QElemType;
typedef struct{
QElemType *data;
int front;
int rear;
int tag;
}SqQueue;
/**
* 入队
* @param Q
* @param x
*/
void EnQueue(SqQueue Q,int x){
if(Q.rear==Q.front && Q.tag==1){ //两个条件都满足才是队满
printf("队满");
}
Q.data[Q.rear] = x;
Q.rear = (Q.rear+1)%maxsize;
Q.tag = 1; //可能队满
}
/**
* 出队
* @param Q
* @return
*/
int DeQueue(SqQueue Q){
int x;
if (Q.rear==Q.front && Q.tag==0){ //两个条件都满足则队空
printf("队空");
}
x = Q.data[Q.front];
Q.front = (Q.front+1)%maxsize;
Q.tag = 0; //可能队空
return x;
}
聊到串,模式匹配不得不说,BF还是KMP都是重点,笔者上传两张BF和KMP算法理解的笔记。BF效率低,逐位比较。KMP效率高,利用next数组。
二叉树可以说是数据结构非常核心地位,二叉树类型有很多,算法也有很多。
/*
* 作者:xulinjie
* 描述:统计树的高度
* 注意:1、树空高度为0 //2、只有根高度为1
*/
#include
#include
typedef char TElemType;
typedef struct BiTNode{
TElemType data;
struct BiTNode *rchild;
struct BiTNode *lchild;
}BiTNode,BiTree;
int GetHeight(BiTree *biTree){
if (biTree == NULL){ //树空
return 0;
} else if (biTree->lchild == NULL && biTree->rchild == NULL){ //只有根
return 1;
} else{ //不是上两种
int hl = 0;
int hr = 0;
hl = GetHeight(biTree->lchild);
hr = GetHeight(biTree->rchild);
if (hl>hr){
return hl+1;
} else{
return hr+1;
}
}
}
/*
* 作者:xulinjie
* 描述:统计二叉树中度为0的结点(叶子结点)
* 思想:度为0即叶子结点
*/
#include
typedef char TElemType;
typedef struct BiTNode{
TElemType data;
struct BiTNode *rchild;
struct BiTNode *lchild;
}BiTNode,BiTree;
int NumsDegree_0(BiTree *biTree){
if(biTree){ //非空结点
if (biTree->lchild==NULL && biTree->rchild==NULL){
return 1;
} else{ //前往下一层
return NumsDegree_0(biTree->lchild)+NumsDegree_0(biTree->rchild);
}
} else{
return 0;
}
}
/*
* 作者:xulinjie
* 描述:交换二叉树中所有结点的左右子树(先换后遍历)
*/
#include
typedef char TElemType;
typedef struct BiTNode{
TElemType data;
struct BiTNode *rchild;
struct BiTNode *lchild;
}BiTNode,BiTree;
void SwapTree(BiTree *biTree){
BiTree *p;
if (biTree!=NULL){
//交换
p = biTree->lchild;
biTree->lchild = biTree->rchild;
biTree->rchild = p;
//遍历
SwapTree(biTree->lchild);
SwapTree(biTree->rchild);
} else{
return;
}
}
/*
* 作者:xulinjie
* 描述:建立一棵二叉排序树
* 思想:左小右大思想
*/
#include
#include
typedef char TElemType;
typedef struct BiTNode{
TElemType data;
struct BiTNode *rchild;
struct BiTNode *lchild;
}BiTNode,BiTree;
void BstInsert(BiTree *biTree,int key){
if (biTree == NULL){
biTree = (BiTree *)malloc(sizeof(BiTree));
biTree->data = key;
biTree->lchild = NULL;
biTree->rchild = NULL;
} else if (biTree->data > key){ //左小
BstInsert(biTree->lchild,key);
} else{ //右大
BstInsert(biTree->rchild,key);
}
}
/*
* 作者:xulinjie
* 描述:判断给定的二叉树是否为完全二叉树
* 思想:利用层次遍历思想
* 思路:采用层次遍历思想,将所有结点加入队列(包括空结点)。遇空结点时,查看其后是否有非空结点,如果有,则二叉树不是完全二叉树
*/
#include
/**
* 二叉树存储结构
*/
typedef char TElemType;
typedef struct BiTNode{
TElemType data;
struct BiTNode *rchild;
struct BiTNode *lchild;
}BiTNode,BiTree;
/**
* 队列存储结构
*/
typedef int QElemType;
typedef struct{
QElemType *base;
int front;
int rear;
}SqQueue;
void InitQueue(SqQueue Q){} //初始化队列
void EnQueue(SqQueue Q,BiTree *biTree){} //入队
void DeQueue(SqQueue Q,BiTree *p){} //出队
int IsEmpty(){} //判队列是否为空
int IsComplete(BiTree *biTree){
BiTree *p;
SqQueue Q;
InitQueue(Q);
if(!biTree){ //空树为完全二叉树
return 1;
}
EnQueue(Q,biTree); //将根结点入队
while(!IsEmpty()){
DeQueue(Q,p); //出队
if(p){ //出队结点非空,将其左右子树入队(注意:空结点也入队)
EnQueue(Q,p->lchild);
EnQueue(Q,p->rchild);
} else{ //出队结点为空,检查其后是否有非空结点,若有,则不是完全二叉树
while (!IsEmpty()){
DeQueue(Q,p);
if (p){ //结点非空,不是完全二叉树
return 0;
}
}
}
}
return 1;
}
/*
* 作者:xulinjie
* 描述:判断两棵二叉树是否相同
* 思想:递归
*/
#include
#include
typedef char TElemType;
typedef struct BiTNode{
TElemType data;
struct BiTNode *rchild;
struct BiTNode *lchild;
}BiTNode,BiTree;
int JudgeBitree(BiTree *biTree1,BiTree *biTree2){
if (biTree1 == NULL && biTree2 == NULL){ //两棵树都为空,则相同
return 1;
} else if (biTree1 == NULL || biTree2 == NULL ||biTree1->data != biTree2->data){ //一棵为空一棵不为空,不同。 数据不同,不同
return 0;
} else{
return (JudgeBitree(biTree1->lchild,biTree2->lchild)*JudgeBitree(biTree1->rchild,biTree2->rchild));//如果不同,则0*1或0*0或1*0
}
}
/*
* 作者:xulinjie
* 描述:非递归先序遍历
* 思想:利用栈的思想
* 思路:1、遇到一个结点,访问它,然后把它压栈,并去遍历它的左子树
* 思路:2、当左子树遍历结束后,从栈顶弹出该结点,并将其指向右儿子,继续1步骤
* 思路:3、当所有结点访问完成,即最后访问的结点为空且栈空时,停止。
*/
#include
typedef char TElemType;
typedef struct BiTNode{
TElemType data;
struct BiTNode *rchild;
struct BiTNode *lchild;
}BiTNode,BiTree;
int isEmpty(){} //判断栈是否为空
void visit(BiTree *node){} //输出当前结点的值
void push(BiTree *node){} //压栈
BiTree *pop(){} //出栈
void PreOrder(BiTree *biTree){
BiTree * node = biTree;
while (node || (!isEmpty())){
while (node){
visit(node);
push(node);
node = node->lchild;
}
if(!isEmpty()){
node = pop();
node = node->rchild;
}
}
}
图相对比较难,图主要在于对相对应的算法的理解以及概念的理解,笔者会对概念做详细解释。
图的遍历常见有DFS和BFS,笔者就针对这两个算法讲讲他们的区别和注意点,具体实现和步骤不再赘述。
深度优先遍历:利用栈实现
- 任意点开始都可以
- 如果该结点与相连的所有点都已经访问过了,则回退到前一个结点,找没访问过的
- DFS相对于树的先序遍历
- DFS生成树高度比BFS生成树的高度:大或相等
广度优先遍历:利用队列实现
- 任意点开始都可以
- 同层顺序随意
- BFS相对于树的层次遍历
- BFS生成树高度比DFS生成树高度:小或相等
为什么DFS生成树高度比BFS生成树的高度:大或相等?
相等情况:只有一个顶点的图
为什么大:因为DFS是深度扩展、BFS是广度扩展。
最小生成树有n个顶点,n-1条边,也称最小代价树,常用Prim和Kruskal算法来求,笔者就讲讲两个算法区别和注意点,具体算法流程不再赘述。
排序重要性不必说了吧,学排序我认为必须去分类学习,下面我就按分类写相关排序算法
思想:将一个记录插入到已经排序好的有序表中,从而得到一个新的、记录数增1的有序表。
其中算法中有提到哨兵,其作用有两点
1、主要作用是为了防止下标遍历j越界;因为原本的for循环是需要条件j>=0,而如果将要插入元素存入A[0]中,只需要判断A[0] 2、还有一个作用就是防止数据丢失;因为当A[i]要插入时,会需要向后移动元素,这样就会把A[i]覆盖。当然也可以设一个临时变量,所以这个不是主要原因。
/*
* 作者:xulinjie
* 描述:直接插入排序
* 思想:在一个已经有序的序列中,插入数据,重新有序。
* 注意:0号位空出(哨兵)
*/
#include
int InsertSort(int A[],int n){
int j = 0;
//从2开始是因为认为第一个已有序
for (int i = 2; i <=n; ++i) {
if (A[i]<A[i-1]){
//哨兵,A[0]不存元素
A[0] = A[i];
A[i] = A[i-1];
for (j = i-2; A[0]<A[j] ; --j) {
A[j+1] = A[j];
}
A[j+1] = A[0];
}
}
//输出排序后结果
for (int k = 1; k <=n; ++k) {
printf("%d",A[k]);
}
}
int main() {
int A[6] = {0,2,5,6,8,2};
InsertSort(A,5);
//结果:22568
return 0;
}
思想:利用二分查找法思想;
当遇到相同关键字,插在后面;
相较于直接插入排序,改善了比较次数,但不能改变移动次数;
/*
* 作者:xulinjie
* 描述:折半插入排序
* 思想:折半插入(折半查找思想)。
* 注意:相对于直接插入排序,改善了比较次数,不能改善移动次数
*/
#include
void HalfSort(int A[],int n){
int low,high,mid;
int i,j; //计数变量
for (i = 2; i <= n; ++i) {
A[0] = A[i]; //A[i]暂存到A[0]中
low = 1;
high = i-1;
while (low <= high){
mid = (low+high)/2;
if (A[mid]<A[0]){ //查左半子表
high = mid - 1;
} else{
low = mid + 1; //查右半子表
}
}
//统一后移,空出插入位置
for (j = i-1; j>=high+1; --j){
A[j+1] = A[j];
}
A[high+1] = A[0];
}
//结果递减
for (int k = 1; k <= n; ++k) {
printf("%d",A[k]);
}
}
int main() {
int A[6] = {0,2,5,6,8,2};
HalfSort(A,5);
//结果:86522
return 0;
}
希尔排序与直接插入排序的区别:
1、前后记录的增量是dk,不是1
2、A[0]只是暂存单元,不是哨兵,所以要设置j>0防止下标越界
/*
* 作者:xulinjie
* 描述:希尔排序
* 思想:类似于直接插入排序,但前后记录的增量是dk而不是1
* 注意:A[0]作为暂存单元
*/
#include
void ShellSort(int A[],int n){
int i,j; //计数变量
for (int dk = n/2; dk >= 1; dk=dk/2) { //步长变化
for (i = dk+1; i <= n ; ++i) {
if (A[i]<A[i-dk]){ //需将A[i]插入到有序增量表中
A[0] = A[i]; //暂存到A[0]中
for (j = i-dk; j>0&&A[0]<A[j]; j-=dk) {
A[j+dk] = A[j];
}
A[j+dk] = A[0];
}
}
}
for (int k = 1; k <=n; ++k) {
printf("%d",A[k]);
}
}
int main() {
int A[6] = {0,2,5,6,8,2};
ShellSort(A,5);
//结果:22568
return 0;
}
思想:将两个相邻的元素进行比较,大的换到右边;
思路:第一次比较第一个和第二个元素,大的换到右边;第二次比较第二个和第三个元素,大的换到右边…
思路:首先任意选取一个记录(通常选第一个记录作为枢轴,将所有关键字较它小的记录都放置在它的位置之前,比它大的放置在它的位置之后)
注意:顺序越乱越快;顺序越有序,反而慢
/*
* 作者:xulinjie
* 描述:快速排序
* 思想:1、确定枢轴 2、右与枢轴比较 3、左与枢轴比较
* 注意:需要多趟才能完成排序
*/
#include
void partition(int A[], int low , int high , int n){
//将当前表中第一个元素设为枢轴值,对表进行划分
int pivot = A[low];
while (low<high){
//枢轴值比A[high]小,没问题,high--
while (low<high&&A[high]>=pivot){
--high;
}
//当枢轴值大于high时,将high位置的元素移动到左端
A[low] = A[high];
//同上
while (low<high&&A[low]<=pivot){
++low;
}
A[high] = A[low];
}
//枢轴元素存放到最终位置
A[low] = pivot;
for (int i = 0; i < n; ++i) {
printf("%d",A[i]);
}
}
int main() {
int A[6] = {3,2,5,6,8,2};
partition(A,0,5,6);
//第一趟结果:223685
return 0;
}
思想:第一次把n个数中的最小的放到第一位;第二次把n-1个数中的最小的放到第二位…
/*
* 作者:xulinjie
* 描述:简单选择排序
* 思想:每一趟在后面的n-i+1个中选出关键码最小的对象,作为有序序列的第i个记录
* 注意:每一次都拿最小的插入即可,和直接插入排序的区别是直接插入排序是随机拿出一个去比较插入。
*/
#include
void SelectSort(int A[],int n){
int min = 0;//记录最小值数据下标变量
int t; //交换数据变量
//n-1趟
for (int i = 0; i < n-1; ++i) {
min = i; //记录最小位置
for (int j = i+1; j < n; ++j) { //在i~n-1中找到最小值元素
if (A[min]>A[j]){ //更新最小元素位置
min = j;
}
}
if (min != i){ //与第i个位置交换
t = A[i];
A[i] = A[min];
A[min] = t;
}
}
for (int k = 0; k < n; ++k) {
printf("%d",A[k]);
}
}
int main() {
int A[6] = {0,2,5,3,8,4};
SelectSort(A,6);
//结果:023458
return 0;
}
学习堆排需要解决两个问题,解决办法比较简单,文章不再赘述
判断一个数据序列是否是小根堆,可以参考以下算法:
/*
* 作者:xulinjie
* 描述:判断数据序列是否是小根堆
*/
#include
void IsMinHeap(int A[], int n){
//n为偶数,即有一个单分支结点
if(n%2==0){
//判断单分支结点
if(A[n/2]>A[n]){
printf("不是小根堆");
return;
}
for (int i = n/2-1; i >= 1; --i) {
if (A[i]>A[2*i] || A[i]>A[2*i+1]){
printf("不是小根堆");
return;
}
}
}
//n为奇数,即没有单分支结点
else{
for (int i = n/2; i >= 1; --i) {
if (A[i]>A[2*i] || A[i]>A[2*i+1]){
printf("不是小根堆");
return;
}
}
}
printf("是小根堆");
}
int main() {
int A[6] = {1,2,3,4,5,6};
IsMinHeap(A,6);
//第一趟结果:223685
return 0;
}
归并排序其实用的是一种分治的思想
/*
* 作者:xulinjie
* 描述:2路归并排序
* 思想:分治思想
* 注意0:表A的两段,A[low..mid],A[mid+1..high]各自有序
* 注意1:最后两个while,只有一个会执行
* 注意2:B只是辅助数组,B有两段,分别比较,小的进A,之后会有一段多余,直接进A
*/
#include
int B[6] = {0};
void MergeSort(int A[],int low, int mid,int high){
int j,k,h; //计数变量
for (int i = low; i <=high; ++i) { //将A中所有元素赋值到辅助数组B中
B[i] = A[i];
}
for (j = low,k = mid+1,h = j; j<=mid&&k<=high; ++h) { //h是归并之后数组下标计数器
if (B[j]<=B[k]){
A[h] = B[j++];
} else{
A[h] = B[k++];
}
}
//若第一个表未检测完,赋值
while (j<=mid){
A[h++] = B[j++];
} //若第二个表未检测完,赋值
while (k<=high){
A[h++] = B[k++];
}
for (int l = 0; l < 6; ++l) {
printf("%d",A[l]);
}
}
int main() {
int A[6] = {3,4,5,1,2,7};
MergeSort(A,0,2,5);
//第一趟结果:123457
return 0;
}
查找中一个很重要的知识点就是平均查找长度,包括查找成功/不成功,一般情况都会算等概率。
思想:引入哨兵,从后往前找;以下是伪码
/*
* 作者:xulinjie
* 描述:顺序查找
* 思想:规定哨兵,从后往前找
*/
#include
typedef struct{
int *data;
int tablelen;
}STable;
int search(STable st , int key){
st.data[0] = key; //哨兵
int i;//下标计数
for ( i = st.tablelen; st.data[i]!=key ; --i); //从后往前找
return i;
}
折半查找的基本算法就不多赘述,比较简单,以下是折半查找的递归算法,可以参考。
说到递归,一个递归算法的成立条件必须清楚:
1、一个问题可以分解成具有相同解决思路的子问题,子子问题,换句话说这些问题都能调用同一个函数
2、必须要有终止条件
/*
* 作者:xulinjie
* 描述:折半查找的递归算法
* 思想:递归思想+折半的思想
*/
#include
int search(int R[],int low,int high,int key){
if(low>high){ //递归的结束条件,也是查找的结束条件
printf("递归结束");
return -1;
}
int mid = (low+high)/2;
if (R[mid]==key){ //查找成功
return mid;
} else if (key<R[mid]){
return search(R,low,mid-1,key);
} else{
return search(R,mid+1,high,key);
}
}
散列表一个很明显的特点就是:不用通过大量无效的比较就能找到关键字位置(即对散列表查找的时间复杂度为O(1),与表中元素个数无关)
思想:1、构造哈希函数;2、解决冲突
解决冲突常用有三种方法:1、线性探测再散列(不断往后找);2、二次线性探测再散列(先右1,再左1,右22,左22,右32,左32…);3、链地址法(同一个链表中,关键字自小至大有序)
填装因子 = n/m。n:关键字个数,m:表长
该篇文章篇幅较长,但也没有讲到每一个知识点,讲了部分笔者认为较为重要的点,适合有一定基础的同学,作为复习通览效果更佳。笔者水平有限,如有错误,还请指正!
《数据结构-严蔚敏版》
《大话数据结构》
https://baike.baidu.com/item/数据结构/1450#3
https://www.cnblogs.com/bigdata-stone/p/10464243.html
https://zhuanlan.zhihu.com/p/94748605