数据结构概述

数据结构概述:

1.数据结构学习方法:

心态勇于直面问题,多听多练,主动思考,多尝试

2.数据的逻辑结构

线性结构、层次结构、网状结构

3.数据结构的意义

提升自己的编程能力

可复用性高、可读性好、效率高、可维护性好

4.数据结构的基本概念

(1)数据

数据即信息的载体,是能够输入到计算机中被计算机识别、存储和处理的符号的总称

(2)数据元素

数据元素是数据的基本单位,又称为记录,数据元素由若干基本项(或称字段、域、属性)组成

5.数据的逻辑结构

集合、线性结构、树形结构、图状结构

6.存储结构

顺序存储、索引存储、链式存储、散列存储

7.线性表

包含若干元素的一个线性序列

记为:L={a0,a1,.....,ai-1,ai,.......an-1}

L为表名,其中为数据元素,n为表长,n>0时为非空表,否则为空表

用二元组形式表示为:L=(D,R)

线性表的特征:

表头无前趋,表尾无后继,其它元素有且仅有一个直接前趋和直接后继

8.顺序存储:

将线性表中的各个元素依次存储在一片连续的内存空间,称之为顺序存储

特征:

(1)逻辑上依次排列,相邻的两个元素物理位置也是相邻的

(2)对数据元素ai的存取为随机存取或按地址存取

(3)存储密度高

存储密度=数据结构中元素所占存储空间/整个数据结构所占空间

顺序存储方便查找,但也有对表的插入和删除等运算对时间的复杂度较差的缺点

线性表的顺序存储结构:

#define N 100

typedef int data_t;

typedef struct

{

data_t data[N];//表的存储空间

int last;

} sqlist,*sqlink;

9.代码规范与顺序表的实现:

代码模块化编程:

sqlist.h sqlist.c test,c

优点:结构清晰明了、软件复用性好

当项目较大时,要编译的文件较多时,每一个.c文件都是要单独处理的,每一个.c文件生成的目标文件的过程是独立的。都要经过预处理、编译、汇编这三步,分别生成各自的.o目标文件,最后所有的目标文件加上库文件一起链接成可执行文件。

可以使用gcc *.c -o test将所有的.c文件生成可执行文件test

调用NULL指针要包含C库文件stdio.h

add:

Linux保存命令:

:w 保存文件但不退出vi

:w file 将修改另外保存到file中,不退出vi

:w! 强制保存,不推出vi

:wq 保存文件并退出vi

:wq! 强制保存文件,并退出vi

:q 不保存文件,退出vi

:q! 不保存文件,强制退出vi

:e! 放弃所有修改,从上次保存文件开始再编辑

顺序表的实现:

函数接口:

list_create();

①申请内存 申请相应数据类型的大小的一块内存,并将malloc的返回值强制类型转换为需要的类型,接下来要判断malloc内存申请是否成功,如果失败返回NULL,即线性表指针不指向任何空间,成功返回申请后得到的内存的首地址,并且如果申请内存成功,线性表指针指向该片内存,创建的内存空间初始状态为空

②初始化 使用memset函数(要包含string.h头文件)可以从起始地址往后的一片区域插入数据,这里插入0,最后标志位last = -1,表示该线性表为空表,写程序时一定要注意这两句话的顺序,先memset再last,否则last会被再次清0

③函数返回值

返回创建好的线性表指针L

list_delete();//顺序表空间销毁

复制代码
int list_delete(sqlink L)
{
•    if(L==NULL)
​
•    {
•        return -1;
•    }
​
•    free(L);
​
•    L = NULL;
​
•    return 0;
}

list_insert();

函数原型:

int list_insert(sqlink L,data_t value,int pos);

调用:list_insert(L,x,pos);

①:判断该线性表是否已满?

②:pos是否属于[0,last+1]区间 //插入规则

③:元素移动(后面元素先向后移动,依次往前)

④:存新值 last++

list_clear();//顺序表的清空

复制代码
int list_clear(sqlink L)
​
{
​
•    if(L==NULL)
​
•    {
​
•        return -1;
​
•    }
​
•    memset(L, 0, sizeof(sqlist));
​
•    L ->last = -1;
​
•    return 0;
}

list_deleteP(L,pos);//删除线性表中的元素

〇:判断是否为空表

①:检查合法性。判断要删除的位pos是否在[0,last]区间内

②:移动。被删除元素的后面紧邻元素先移动,后面元素都依次往前移动

③:更新last--

复制代码
int list_deleteP(sqlink L,int pos)
​
{
​
•    int i;
​
•    //判断表是否为空表
​
•    if(L->last == -1)
​
•    {
​
•        printf("list is empty\n");
​
•        return -1;
​
•    }
​
•    //pos的合法性
​
•    if(pos<0 || pos>L->last)
​
•    {
​
•        printf("delete pos is invalid\n");
​
•        return -1;
​
•    }
​
​
​
•    //move
​
•    for(i=pos+1;i<=L->last;i++)
​
•    {
​
•        L->data[i-1] = L->data[i];
​
•    }
​
•    //update
​
•    L->last--;
​
•    return 0;
​
}

C语言中不允许函数名称相同

list_purge();//去重复元素

复制代码
int list_purge(sqlink L)
{
•    int i,j;
​
•    if(L->last == 0)//单个元素不比较,直接退出
​
•    {
•       return 0;
•    }
​
•    i=1;
​
•    while(i<=L->last)
•    {
​
•        j=i-1;
​
•        while(j>=0)
​
•        {
•            if(L->data[j] == L->data[i])
​
•            {
•                list_deleteP(L,i);
​
•                break;//跳出本层循环
•            }
​
•            else
​
•            {
•                j--;
•            }
​
•        }
​
•        if(j<0)
​
•        {
•           i++;
•        }
•    }
​
•    return 0;
}

list_merge()//两个顺序表的归并

复制代码
int list_merge(sqlink L1,sqlink L2)
​
{
​
•    int i=0;
​
•    int ret;
​
•    while(i<=L2->last)
​
•    {
​
•        if((ret = list_locate(L1,L2->data[i]))==-1)
​
•        {
​
•            if((list_insert(L1,L2->data[i],L1->last+1))==-1)
​
•            {
​
•                return -1;
​
•            }
​
•        }
​
•        i++;
​
•    }
​
•    return 0;
​
}

list_show();//顺序表的遍历

复制代码
int list_show(sqlink L)
​
{
​
•    int i;
​
•    if(L==NULL)
​
•    {
​
•        printf("can not show\n");
​
•        return -1;
​
•    }
​
•    if(L->last == -1)
​
•    {
​
•        printf("the list is empty\n");
​
•    }
​
•    for(i=0;i<=L->last;i++)
​
•    {
​
•        printf("%d  ",L->data[i]);
​
•    }
​
•    puts("");
​
•    return 0;
​
}

list_empty();//判断顺序表是否为空

复制代码
int list_empty(sqlink L)
​
{
​
•    if(L->last == -1)
​
•    {
​
•        return 1;
​
•    }
​
•    else
​
•        return 0;
​
}

list_length();//求顺序表的长度

复制代码
int list_length(sqlink L)
​
{
​
•    if(L==NULL)
​
•    {
​
•        return -1;
​
•    }
​
•    return (L->last+1);
​
}
​
​

list_locate();//定位元素位置

复制代码
int list_locate(sqlink L,data_t value)

{

​    int i=0;

​    for(;i<=L->last;i++)

​    {

​        if(L->data[i] == value)

​        {

​            return i;

​        }

​    }

​    return -1;

}

顺序表的优缺点:

优点:存储密度高、便于存取数据

缺点:要求必须是一片连续的内存空间,而且进行插入、删除等操作时耗时,可能会有大片元素移动的现象。

什么时候选择使用顺序表:

当涉及到查找、更新数据较多时,使用顺序表效率较高

编程时要先画图,理清思路,再去写程序

线性表之链表

(多画图理解)

链式存储:

将线性表L=(a0,a1,a2,....,an-1)的各元素存储在内存中的不同模块,称为结点,通过地址或指针建立元素之间的联系。

通常会引入一个头结点来指向第一个节点,方便操作,且头节点数据不重要,只要存放的是第一个结点的地址即可

结点类型描述:

typedef struct node

{

data_t data;//节点的数据域

struct node *next;//节点的后继指针域

}listnode,*linklist;

上述图片中结点放在栈上,定义好A后系统自动分配好内存空间,然后再把A结点的地址赋予结点指针P,栈上的内存有着作用域,超出作用域后内存释放

P访问成员时必须用箭头,结构体A访问成员时用" ."

若P为NULL,访问操作成员时会出现段错误

注:一般的段错误是由于指针的非法访问或者内存越界所致

下述图片采用动态内存申请的方式申请一块内存,申请灵活,需要多少申请多少,结点放到堆上

建立单链表:

依次读入表L={a0,a1,...,an-1}中的每一个元素ai,若a[i] 不等于结束符-1,则为a[i]创建一个结点,然后插入表尾,最后返回链表的头结点指针H

链表的创建是动态形成的,算法运行前,表结构不存在

复制代码
linklist list_create()

{

​    linklist H;

​    H = (linklist)malloc(sizeof(listnode));

​    if(H == NULL)

​    {

​       printf("malloc failed\n");

​       return H;

​    }

​    H->data =  0;

​    H->next = NULL;


​    return H;
}

空链表的表示:

一般赋初值时,将data置为0,next置空

复制代码
int list_tail_insert(linklist H,data_t value)
{

​    if(H == NULL)
​    {

​      printf("H is NULL\n");

​      return -1;
​    }

​    //创建新节点

​    linklist p,q;


​    //内存申请

​    if((p=(linklist)malloc(sizeof(listnode))) == NULL)

​    {

​       printf("malloc failed\n");

​       return -1;

​    }

​    //申请成功后赋予初值

​    p->data = value;

​    p->next = NULL;



​    //定位尾节点

​    q = H;

​    while(q->next!=NULL)
​    {
​       q = q->next;
​    }

​    //尾部插入

​    q->next = p;

​    return 0;
}

list_show();

复制代码
int list_show(linklist H)

{

​    if(H == NULL)

​    {

​      printf("H is NULL\n");

​      return -1;

​    }

​    linklist p;

​    p = H;//忘记赋值的话,段错误

​    while(p->next != NULL)

​    {

​       printf("%d ",p->next->data);

​       p = p->next;

​    }

​   puts("");

   return 0;

}

基本运算的相关算法:

①:list_get(H,pos);

//链表查找

约定头结点位置为-1,往后依次是0,1,2,......

复制代码
linklist list_get(linklist H,int pos)
{
​    linklist p;

​    int i;

​    if(H == NULL)

​    {

​      printf("H is NULL\n");

​      return NULL;

​    }

​    if(pos==-1)

​    {
​       return H;
​    }

​    p = H;

​    i =-1;

​    while(i<pos)
​    {
​      p = p->next;

​      if(p == NULL)

​      {

​        printf("pos is invalid\n");

​        return NULL;

​      }
  
​      i++;
​    }
​  
​    return p;
}

p = list_get(H,4);

printf("value=%d\n",p->data);

上图截图出现段错误,是由于上述代码段出现p->data的情况,返回空指针,空指针又去访问数据出现段错误,非法访问。

②:链表的插入

源码:

复制代码
int list_insert(linklist H,data_t value,int pos)

{
​    linklist p,q;

​    if(H == NULL)

​    {

​       return -1;

​    }


​    //查找要插入的前一个结点,并返回其指针

​    p = list_get(H,pos-1);

​    if(p == NULL)

​    {

​       return -1;

​    }

​  

​    //新建新结点

​    if((q=(linklist)malloc(sizeof(listnode))) == NULL)

​    {

​        printf("malloc  failed\n");

​        return -1;

​    }



​    //给新结点赋予初值

​    q->data = value;

​    q->next = NULL;


​    //插入

​    q->next = p->next;

​    p->next = q;

​    return 0;

}

指针指向改变有先后顺序,先使 q的指针域指向ai,后使p的指针域指向新插入的元素x,即p ->next = q,否则会出现链表断掉的情况

单链表的优缺点:

优点:单链表不需要大量连续的存储空间,通过指针对元素进行操作,插入、删除时元素不需要成片移动,此时执行效率高,同时可以由用户动态申请内存,不会造成内存的浪费。

缺点:单链表查找时从头结点开始,并且只能单向移动,不能随机访问,比较耗时,效率低。

③:链表的结点删除

①:删除操作前需要调用链表查找接口函数list_get();,找到所要删除结点的直接前驱,假设用指针p指向该前驱结点

②:(1)定义一个临时变量q,用来存放所要删除结点的起始地址,即q = p->next ,便于后面的内存释放

(2)改变结点的指针指向,p->next = q->next

(3) 释放内存,合理利用内存,可持续发展,避免内存泄漏: free(q)

结点删除函数接口:

复制代码
int list_delete(linklist H,int pos)

{
​    linklist p,q;

​    //check

​    if(H == NULL)

​    {

​       printf("H is NULL\n");

​       return -1;

​    }


​    //locate prior

​   
​    p = list_get(H,pos-1);


​    //check valid?

​    if(p == NULL)

​    {

​       return -1;

​    }

​    if(p->next == NULL)//特殊情况,删除最后一个结点的后一个结点

​    {

​       printf("delete pos is invalid\n");

​       return -1;

​    }


​    //update

​    q = p->next;

​    p->next = q->next;//p->next = p->next->next;


​    //free

​    printf("delete:%d\n",q->data);

​    free(q);

​    q = NULL;//避免野指针!

​    return 0;
}

main函数中删除结点的实现:

复制代码
int main()

{

​    linklist H;

​    int value;

​    H = list_create();

​    if(H == NULL)//链表创建失败

​    {

​      return -1;

​    }

​    

​    printf("input:\n");

​    while(1)

​    {

​       scanf("%d",&value);

​       if(value == -1)
​       {

​          break;//结束链表创建

​       }

​       list_tail_insert(H,value);

​       printf("input\n");

​    }

​    list_show(H);

​    
​    list_delete(H,-1);//1,3,5,7


​    list_show(H);

​    return 0;

}

④:链表的释放:

复制代码
int list_free(linklist H)

{
​    linklist p;

​    if(H == NULL)

​    {

​      printf("H is NULL\n");

​      return -1;

​    }

​    p = H;

​    printf("free:");


​    while(H != NULL )

​    {

​       p = H;

​       printf("%d ",p->data);

​       H = H->next;

​       free(p);//不能把释放内存放到前面

​    }

​    puts("");    

​    return 0;
}

如果主函数中重复调用list_free()函数,则会出现下面的情况:如下分别是主函数调用和命令行显示结果:

复制代码
int main()

{

​    linklist H;

​    int value;

​    H = list_create();

​    if(H == NULL)//链表创建失败

​    {

​      return -1;

​    }

​    

​    printf("input:\n");

​    while(1)

​    {

​       scanf("%d",&value);

​       if(value == -1)

​       {

​          break;//结束链表创建

​       }

​       list_tail_insert(H,value);

​       printf("input\n");

​    }

​    list_show(H);





​    printf("H=%p\n",H);

​    list_free(H);

​    printf("H=%p\n",H);





​    list_delete(H,-1);





​    list_show(H);





​    list_free(H);

​    return 0;

}

改进方法:

1:将list_free()函数的函数声明和定义部分的返回值改为linklist(结点指针型),list_free函数体最后返回一个空指针NULL,并在main函数中用H接收,H= list_free(H);,这样所有的内存被释放之后,又被赋予了NULL,就无法再操作内存

假如在list_free()函数最后,使用H = NULL的方式赋予空指针,这样是没用的,因为主函数中,调用list_free函数时,实参为临时变量H,形参为被调用函数的形参,改变形参的值,实参不会变

2:或者list_free传参时传二级指针,即&H,这种方法较上面的麻烦,但也行得通:

注意二级指针访问加变为一级指针访问成员,H->next,这样是错误的,会出现以下结果:(正确的做法是在H两边加上括号,因为->的优先级高于*)

error: request for member 'next' in something not a structure or union

链表的复杂运算:

⑤:链表的反转

思路:

代码实现:

复制代码
int list_rev(linklist H)

{

​    linklist p,q;

​    if(H == NULL)

​    {

​        printf("H is NULL\n");

​        return -1;

​    }


​    if(H->next == NULL ||(H->next)->next == NULL)

​    {

​        printf("only one\n");

​        return 0;

​    }

​    p = H->next->next;

​    H->next->next = NULL;


​    while(p != NULL)

​    {
​        q = p;

​        p = p->next;

​        q->next = H->next;

​        H->next = q;

​    }

​    return 0;
}

问题:

1.为什么不能用一个指针P来操作?

只用指针p操作时,要负责两个任务,第一遍历所有元素,其二完成插入到H后的任务,插入H后时,p的next已经被赋新值了,不能再往后遍历了,链表已经断了

2.既然形参不能改变实参,那为什么可以通过操作形参改变成员的方式去改变链表

因为传进来的参数是地址,可以通过地址访问目标结构体,址传递可以改变操控原值

3.上面的链表的反转是取出链表中的每一个节点后头部插入,改变了原有链表的结构。

那么来看一个题目:提供一个单向链表的链表首地址,实现反向打印链表的数据,要求不能改变原有链结构。

(注意要求不能改变链表的结构)

复制代码
#include <stdio.h>
#include <stdlib.h>

typedef int data_t;

typedef struct node
{
	data_t data;
	struct node *next; 
}listnode,*linklist;

linklist list_create();
int list_show(linklist H);
int list_tail_insert(linklist H,data_t value);
int list_rev(linklist H);

int main(int argc, const char *argv[])
{
	linklist H;

	if((H = list_create()) == NULL)
	{
		return -1;
	}

	list_tail_insert(H,1);
	list_tail_insert(H,2);
	list_tail_insert(H,3);
	list_tail_insert(H,5);
	list_tail_insert(H,7);

	list_show(H);

	list_rev(H->next);
	puts("");

	list_show(H);


	return 0;
}

linklist list_create()
{
	linklist H;

	if((H = (linklist)malloc(sizeof(listnode))) == NULL)
	{
		perror("list_create-malloc");
		return NULL;
	}

	H->data = 0;
	H->next = NULL;


	return H;
}

int list_tail_insert(linklist H,data_t value)
{
	linklist p = NULL;

	if(H == NULL)
	{
		printf("list_tail_insert:H is NULL\n");
		return -1;
	}

	if((p = (linklist)malloc(sizeof(listnode))) == NULL)
	{
		perror("list_tail_insert-malloc");
		return -1;
	}
	
	p->data = value;
	p->next = NULL;

	while(H->next != NULL)
	{
		H = H->next;
	}

	H->next = p;

	return 0;
}

int list_show(linklist H)
{
	while(H->next)
	{
		printf("%d ",H->next->data);
		H = H->next;
	}

	puts("");

	return 0;
}

int list_rev(linklist H)
{
	/*
	linklist p,q;

	p = H->next->next;

	H->next->next = NULL;

	while(p != NULL)
	{
		q = p;
		p = p->next;
		q->next = H->next;
		H->next = q;
	}
	*/

	/*或者可以这样写*/
	if(H != NULL)
	{
		list_rev(H->next);
		printf("%d ",H->data);
	}

	return 0;
}

运行结果如下:

复制代码
linux@linux:~/ds/liner$ gcc test.c
linux@linux:~/ds/liner$ ./a.out
1 2 3 5 7 
7 5 3 2 1 
1 2 3 5 7 

代码思路:

递归实现在不改变链表结构的前提下,逆序打印链表数据

⑥:链表的相邻两结点之和的最大值

思路:

接口函数源码:

复制代码
linklist list_adjmax(linklist H,data_t *value)

{

​    linklist p,q,r;

​    data_t sum;

​    if(H == NULL)

​    {

​        printf("H is NULL\n");

​        return NULL;

​    }


​    if(H->next == NULL || H->next->next == NULL ||H->next->next->next == NULL)

​    {

​         return H;

​    }


​    q = H->next;

​    p = q->next;

​    r = q;

​    sum = q->data + p->data;



​    while(p->next != NULL)

​    {

​       p = p->next;

​       q = q->next;

​       if(sum< q->data + p->data)

​       {

​          sum = q->data + p->data;

​          r = q;

​       }

​    }

​    *value = sum;//将sum的值带出函数

​    return r;//注意不是返回q,因为r不一定要更新
}

⑦:有序链表的合并

思路:

①:

②:

③:

退出循环体后,再判断p和q,一方为空,就把另一方直接插入到r后面

源码如下:

复制代码
int list_merge(linklist H1,linklist H2)

{

​    linklist p,q,r;

​    if(H1 == NULL || H2 == NULL)

​    {

​        return -1;

​    }





​    p = H1->next;

​    q = H2->next;

​    r = H1;

​    H1->next = NULL;

​    H2->next = NULL;





​    while((p != NULL) && (q != NULL)) //while(p && q);

​    {

​        if(p->data <= q->data)

​        {

​            r->next = p;

​            p = p->next;

​            r =r->next;

​            r->next = NULL;

​        }

​        else

​        {

​            r->next = q;

​            q = q->next;

​            r =r->next;

​            r->next = NULL;





​        }

​    }

​    if(p == NULL)

​    {

​        r->next = q;

​    }



​    else

​    {

​        r->next = p;

​    }



​    return 0;

}

作业:无序链表的元素排序: 写一个函数,实现链表的排序(使链表中元素的从无序到有序,要求从小到大);

结果如下:

原因是:这里系统认为是定义的变量没有malloc申请内存,那么直接使用free会出错,比如:(malloc与free要配对使用)

因此free释放的应该是malloc申请的有效空间

特别注意:这里有个陷阱:明明调用函数传递参数传的是指针(节点地址),被调用函数中又使用malloc函数申请堆内存,可为什么会出错呢?

因为指针作为参数传递,其实本质是值传递,传给被调用函数后,会在栈上给形参分配空间,空间里的值就是你所传递的指针值,H1 = list_create()这句话,其实本质是malloc申请堆上的一块内存空间,强制类型转换后给形参H1赋值,形参H1被改变了,但形参不能改变实参,实参H1并未被改变,还是野指针,根本就没有接收到malloc申请的内存空间,因此free直接报错

改进:

方法1:

H1 = list_create这句话不要放被调用函数,改成放到调用函数里

方法2:

list_sort()排序函数由一个参数增加到2个参数,传递一级指针变量的地址,用二级指针变量来接收

复制代码
int list_sort(linklist H,linklist *H1)

{
​    linklist p,q,r,m;


​    if(H == NULL)

​    {
​        printf("list_sort:invalid operation\n");

​        return -1;
​    }


​    if(((*H1) = list_create()) == NULL)

​    {

​        return -1;

​    }


​    m = *H1;

​    while(H->next != NULL)

​    {

​        r = H;

​        p = H->next;

​        q = p;

​        p = p->next;


​        while(p != NULL)

​        {

​            if(p->data < q->data)

​            {

​                q = p;

​                while(r->next != q)

​                {

​                    r = r->next;

​                }

​                p = p->next;

​            }


​            else

​            {

​                p = p->next;

​            }
​        }


​        r->next = q->next;


​        while((*H1)->next)

​        {

​            (*H1) = (*H1)->next;

​        }


​        (*H1)->next = q;

​        printf("*H1:%d\n",(*H1)->data);

​        q->next = NULL;

​    }


​    *H1 = m;



​    /*

​        这种方法比上面简单多了,只需要传一个参数H

​       linklist p,q;


​       if(H == NULL)

​       {

​       printf("list_sort:invalid operation\n");

​       return -1;

​       }


​       p = H->next;

​       q = p;


​       while(q->next != NULL)

​       {

​       while(p->next != NULL)

​       {

​       p = p->next;

​       if(p->data < q->data)

​       {

​       p->data ^= q->data;

​       q->data ^= p->data;

​       p->data ^= q->data;

​       }

​       }

​       q = q->next;

​       p = q;

​       }

​       */

​    return 0;
}
复制代码
int test_sort()

{

​    linklist H,H1;

​    int i;





​    data_t a[] = {9,2,4,0,5,7,1};





​    if((H = list_create()) == NULL)

​    {

​        return -1;

​    }





​    for(i = 0;i < sizeof(a)/sizeof(data_t);i++)

​    {

​        list_tail_insert(H,a[i]);

​    }



​    list_show(H);

​    list_sort(H,&H1);

​    list_show(H1);



​    list_free(&H);

​    list_free(&H1);





​    return  0;

}

注意测试时,list_sort函数里最后H1跑到了倒数第二个位置,实参也会跟着动,所以为了正常输出遍历结果,要使用第三方指针m来接收H1,最后再还给它

(58条消息) free(): invalid pointer的问题雪舞飞扬之殇的博客-CSDN博客free(): invalid pointer

进程退出后,malloc分配的资源会被系统回收_进程退出后占用的资源会被回收吗-CSDN博客?

数组和链表的区别:

答:

逻辑结构上来看,数组必须实现定于固定的长度,不能适应数据动态增减的情况,即数组的大小一旦定义就不能改变。当数据增加时,可能超过原先定义的元素的个数;当数据减少时,造成内存浪费;链表动态进行存储分配,可以适应数据动态地增减的情况,且可以方便地插入、删除数据项。

内存存储的角度看;数组从栈中分配空间(用new则在堆上创建),对程序员方便快速,但是自由度小;链表从堆中分配空间,自由度大但是申请管理比较麻烦。

访问方式类看,数组在内存中是连续的存储,因此可以利用下标索引进行访问;链表是链式存储结构,在访问元素时候只能够通过线性方式由前到后顺序的访问,所以访问效率比数组要低。

栈实现及应用

栈是限制在一端进行插入和删除操作的线性表(俗称堆栈),允许进行操作的一端称为栈顶,另一固定端称为栈底,当栈中没有元素时成为空栈。 特点:后进先出

非线性问题线性化,需要用到栈,另外可以将栈看作一个容器,遵循先入后出或者后进先出,而栈和顺序表有什么不同呢?

顺序表中,元素的插入和删除可以在任意位置进行,而栈只能在栈顶进行压栈和弹出,栈的示意图如下图所示:

(1)顺序栈:

顺序栈定义:

定义一个结构体,取别名为sqstack(顺序栈),结构体中有三个成员,分别为:指向实际栈空间的data_t型指针data、当前栈的最大元素个数以及指示栈顶位置(数组下标)的变量top,见下图:

顺序栈的创建:

sqstack * sqstack_create();

根据定义好的顺序栈结构体sqstack,先定义一个顺序栈结构体指针s,首先申请一块内存用于存储结构体成员,s = (sqstack *)malloc(sizeof(sqstack)); 接着再申请一块内存用于存储实际栈的数据,被结构体成员data指向,即s->data = (data *)malloc(len * sizeof(data_t))

源码:

复制代码
sqstack *sqstack_create(int len)

{

​    sqstack *s;


​    if((s = (sqstack *)malloc(sizeof(sqstack))) == NULL)

​    {

​        printf("malloc sqstack failed\n");

​        return NULL;

​    }

​    if((s->data = (data_t *)malloc(len * sizeof(data_t))) == NULL )

​    {

​        printf("malloc data failed\n");

​        free(s);    //data指向的数据区申请失败时释放掉s
  			 s = NULL;

​        return NULL;

​    }


​    memset(s->data,0,len*sizeof(data_t));//调用memset填充初值0

​    s->maxlen = len;//由用户定义栈的长度

​    s->top = -1;//将栈置空

​    return s;
}

顺序栈的释放:

int sqstack_free(sqstack *s);

顺序栈的释放和malloc配对使用(用户申请的内存使用过后要释放掉,避免内存泄漏),下面为源码:

复制代码
int sqstack_free(sqstack *s)

{

​    if(s == NULL)

​    {

​        printf("s is NULL\n");

​        return -1;

​    }

​    if(s->data != NULL)  //判断数据存储区是否曾经创建成功

​    {

​        free(s->data);

​    }


​    free(s);

​    return 0;
}

这里分为两种情况:

其一:

如果在创建栈时,创建data内存时失败,则要将之前malloc成功的内存(s)释放掉,防止内存泄漏(就程序而言,不写free的话,data申请内存失败后会返回一个NULL给s,这时s被置空,然而里面的成员未被释放,会造成内存泄漏)

其二:

如果整个栈创建成功的话,在上面的栈释放代码中会判断data指向的内存是否申请成功,若是,先则将这段内存释放掉,再释放掉顺序栈指针s

顺序栈的满判断

int sqstack_empty(sqstack *s);

在入栈前要先判断栈里的数据是否为满状态:(top)

可以将栈视为一个装水的容器,上面带有刻度,而top就像是浮标,数据入栈时从栈顶入栈,数据开始从栈底堆叠,水涨船高,top浮标也跟着上浮,当top到达maxlen-1时,数据存储区已满

复制代码
int sqstack_full(sqstack *s)

{

​    if(s == NULL)

​    {

​        printf("s is NULL\n");

​        return -1;

​    }


​    return (s->top == s->maxlen-1 ? 1 : 0);//采用三目运算符,当top指向了maxlen-1,视为满
}

顺序栈的空判断

当s->top等于-1时视为无元素,此时栈为空

源码:

复制代码
int sqstack_empty(sqstack *s)

{

​    if(s == NULL)

​    {

​        printf("s is NULL\n");

​        return -1;

​    }

​    return (s->top == -1 ? 1 : 0);

}

顺序栈的入栈

int sqstack_push(sqstack *s,data_t value);

复制代码
int sqstack_push(sqstack *s,data_t value)

{

​    if(s == NULL)

​    {

​        printf("s is NULL\n");

​        return -1;

​    }





​    if(s->top == s->maxlen-1)

​    {

​        printf("sqstack is full\n");

​        return -1;

​    }





​    s->top++;

​    s->data[s->top] = value;//    *(s->data + s->top) = value;

​    return 0;

}

先将top加1,再将top指向的数据赋值,入栈时先要判断栈是否为满状态

顺序栈的出栈:

data_t sqstack_pop(sqstack *s);

复制代码
data_t sqstack_pop(sqstack *s)

{

​    if(s == NULL)

​    {

​        printf("s is NULL\n");

​        return -1;

​    }





​    s->top--;

​    return (s->data[s->top+1]);

}

出栈时要先判断栈是否处于空状态,下面为在main函数中的调用,需要在main函数里实现

while(!sqstack_empty(s)) //出栈前先判断栈是否为空,需要在main函数里写,由用户判断

{

printf("pop:%d\n",sqstack_pop(s));

}

需要循环调用该接口函数,如果在接口函数里实现的话,while(1)是死循环,跳不出来,命令行会一直显示返回值

(2)链式栈的实现

链式表中头结点可以看作是栈的top指针,但是只在栈顶位置做插入删除,top不会变化

结构体定义体:

复制代码
typedef int data_t;


typedef struct node

{

​    data_t data;

​    struct node *next;

}listnode,*linkstack;

①:链式栈的创建

申请内存,创建一个空链表s,并作初始化,代码如下:

复制代码
linkstack stack_create()

{

​    linkstack s;


​    s = (linkstack)malloc(sizeof(listnode));

​    if(s == NULL)

​    {

​       printf("s is NULL\n");

​       return NULL;

​    }


​    s->data = 0;

​    s->next = NULL;

​    return s;
}

②:压栈

即在头节点s后尾部插入

复制代码
int stack_push(linkstack s,data_t value)
{
     linkstack p;

​    if(s == NULL)

​    {

​       printf("s is NULL\n");

​       return -1;

​    }

​ 
​    p = (linkstack)malloc(sizeof(listnode));

​    if(p == NULL)

​    {

​       printf("p is NULL\n");

​       return -1;

​    }


​    p->data = value;

​    //p->next = NULL;



​    p->next = s->next;

​    s->next = p;


​    return 0;
}

③:出栈

即头节点后栈顶元素释放

复制代码
data_t stack_pop(linkstack s)

{

​    linkstack p;

​    data_t t;





​    p = s->next;





​    s->next = p->next;

​    

​    t = p->data;





​    free(p);

​    p = NULL;





​    return t;

}

示意图:

④:链式栈释放

复制代码
linkstack stack_free(linkstack s)

{
​    linkstack p;

​    
​    if(s == NULL)

​    {

​       printf("s is NULL\n");

​       return NULL;

​    }


​    while(s != NULL)

​    {

​       p = s;

​       s = s->next;

​       printf("free:%d\n",p->data);

​       free(p);

​    }
​    
​    return NULL;
}

示意图:

⑤:判断空状态?

复制代码
int stack_empty(linkstack s)

{
​    if(s == NULL)

​    {

​       printf("s is NULL\n");

​       return -1;

​    }

​    return (s->next == NULL ? 1 : 0);//返回三目运算符结果
}

⑥:查看栈顶元素

复制代码
data_t stack_top(linkstack s)

{

   return (s->next->data);

}

队列

队列:

队列是限制在两端进行插入删除操作的线性表

允许进行存入一端称为"队尾",允许进行删除一端称为"队头";线性表没有元素时成为空队

特点:先进先出

单向队列一端进一端出,双端队列可以两端分别进出元素

顺序循环队列:

结构体定义:

复制代码
typedef int data_t;

#define N 100



typedef struct
{

   data_t data[N];

   int front;

   int rear;

}sequeue;

为什么要使用循环队列?

其一是为了充分利用队列内存空间。当进行队列操作时,队尾指针向后移动,数据元素入队,此时如果队头指针也向后移动时,数据元素出队,就导致出队的位置被空了下来,循环队列可以循环存取数据元素,内存利用率高。

其二,不使用循环队列时,当进行频繁的队列操作时,队尾指针频繁后移,会发生越界,是不被允许的,当使用循环队列时,对数据元素的存取一直在一定范围的内存空间内进行。

设以数组A[m]存放循环队列的元素,其头尾指针分别为front和rear,则当前队列中的元素个数为( )。

A)==(rear-front+m)%m== B)rear-front+1

C)(front-rear+m)%m D)(rear-front)%m

正确答案:A

①:循环队列的创建

先malloc一个内存空间,强制类型转化为顺序队列指针并被sq接收

初始化:调用memset函数,在申请的内存区间内全部用0填充,最后将队列置空,或者用bzero函数

复制代码
sequeue *queue_create()
{
​    sequeue *sq;


​    if((sq = (sequeue *)malloc(sizeof(sequeue))) ==NULL)

​    {

​        printf("malloc failed\n");

​        return NULL;

​    }


​    memset(sq->data,0,sizeof(sq->data));


​    sq->front = 0;

​    sq->rear = 0;


​    return sq;
}

②:队列的空状态判断:

front与rear指针相等即为空

复制代码
int queue_empty(sequeue *sq)

{





​    if(sq == NULL)

​    {

​        return -1;

​    }





​    return (sq->front == sq->rear ? 1 :0);

}

③:队列的满状态判断:

见上图,若(sq->rear +1) %N == sq->front时,即为满状态,循环队列中不是简单的rear堆叠的问题,而是要使rear+1对N取余,才能构成一个循环回路

复制代码
int queue_full(sequeue *sq)

{


​    if(sq == NULL)

​    {

​        return -1;

​    }


​    if(((sq->rear +1) % N) == (sq->front))

​    {

​        printf("enqueue is full\n");

​        return 1;

​    }


​    else

​    {

​        return 0;

​    }

}

④:队列的入队

要先判断队列是否处于满状态,再做赋值处理

复制代码
int enqueue(sequeue *sq,data_t x)

{

​    if(sq == NULL)

​    {

​        return -1;

​    }





​    if(((sq->rear +1) % N) == (sq->front))

​    {

​        printf("enqueue is full\n");

​        return -1;

​    }





​    sq->data[sq->rear] = x;

​    sq->rear = (sq->rear +1) % N;//新的rear值加1对N取余后赋给旧rear,这样就可保证rear在[0.5]区间内

 

​    return 0;

}

红色代码为先存值,再更新rear

⑤:队列的出队

要先用一个第三变量存储先前的值,再对front进行加一取余更新,最后返回第三变量

复制代码
data_t dequeue(sequeue *sq)

{

​    data_t t;





​    if(sq == NULL)

​    {

​        return -1;

​    }



​    t = sq->data[sq->front];





​    sq->front= (sq->front +1) % N;





​    return t;

​    //return (sq->data[(sq->front+N - 1) % N]);

}



注意将代码改为红色部分更为简单

⑥:释放

复制代码
sequeue *queue_free(sequeue *sq)
{


​    if(sq == NULL)

​    {

​        return NULL;

​    }

​    

​    free(sq);

​    sq = NULL;


​    return NULL;
}

链式队列:

队列元素的插入在队尾操作,删除在队头操作,由队尾指针和队头指针分别控制

首先应该定义两个结构体:(结点结构体和链式队列结构体)

typedef int data_t;

typedef struct node

{

data_t data;

struct node *next;

}listnode,*linklist;

typedef struct

{

linklist front;

linklist rear;

}linkqueue;

①:链式队列的创建;

见上图,定义一个lq指针用于接收创建好的链式队列结构体的地址,调用malloc函数申请一段内存,lq指向该内存空间,接着再申请一段内存,将该内存的首地址返回给front,同时front赋给rear,判断是否创建内存空间成功,之后给头结点赋初值。

源码:

复制代码
linkqueue *queue_create()

{

​    linkqueue *lq;

/*    linklist front,rear;


​    if((front = (linklist)malloc(sizeof(listnode))) == NULL)

​    {

​        printf("malloc listnode failed\n");

​        return NULL;

​    }


​    if((lq = (linkqueue *)malloc(sizeof(linkqueue))) == NULL)

​    {

​        printf("malloc linkqueue failed\n");

​        return NULL;

​    }





​    lq->rear = lq->front;

​    lq->front->data = 0;

​    lq->front->next = NULL;

*/


​    if((lq = (linkqueue *)malloc(sizeof(linkqueue))) == NULL)
​    {

​      printf("malloc linkqueue failed\n");

​      return NULL;
​    }


​    if((lq->rear = lq->front = (linklist)malloc(sizeof(listnode))) == NULL)
​    {

​       printf("malloc listnode failed\n");

​       return NULL;

​    }


​    lq->front->data = 0;

​    lq->front->next = NULL;

​    return lq;

}

②:入队

首先创建一个新节点,p为结点指针,然后将p的地址赋给rear的指针域,然后将rear更新,源码如下:

复制代码
int enqueue(linkqueue *lq,data_t x)

{

​    linklist p;

​    if(lq == NULL)

​    {

​        printf("lq is NULL\n");

​        return -1;

​    }





​    if((p = (linklist)malloc(sizeof(listnode))) == NULL)

​    {

​        printf("p is NULL\n");

​        return -1;

​    }





​    p->data = x;

​    p->next = NULL;





​    lq->rear->next = p;

​    lq->rear = lq->rear->next;





​    return 0;

}

③:出队

首先定义一个结点指针p,将front的地址先暂时存放,然后将front更新,同时释放掉p指向的内存空间,同时将p置空,最后返回释放掉的数据。

注:这里采用删除头结点的方式,另外一种方法是删除队头队尾中间的结点,只不过剩最后一个尾部结点时,需要再将rear回调,使rear和front相等。

源码:

复制代码
data_t dequeue(linkqueue *lq)

{

​    linklist p;


​    if(lq == NULL)

​    {

​        printf("lq is NULL\n");

​        return -1;

​    }


​    p = lq->front;

​    lq->front = p->next;


​    free(p);

​    p = NULL;


​    return (lq->front->data);
}

④:判断链式队列是否为空(不用判断满状态,因为malloc进行动态申请内存)

空状态判断依据是:两个链式指针相等,或者front的指针域为NULL

复制代码
int queue_empty(linkqueue *lq)

{

​    if(lq == NULL)

​    {

​        printf("lq is NULL\n");

​        return -1;

​    }





​    return (lq->front == lq->rear ? 1 : 0);//或者返回一个lq->front->next == NULL;

}

⑤:清空队列

保留头结点

复制代码
int queue_clear(linkqueue *lq)

{

​    linklist p;


​    if(lq == NULL)
​    {

​        printf("lq is NULL\n");

​        return -1;
​    }

​    while(lq->front->next != NULL)
​    {

​       p = lq->front;

​       lq->front = p->next;

​       printf("clear-free:%d\n",p->data);

​       free(p);

​       p = NULL;

​    }


​    return 0;
}

⑥:释放队列

全部释放

注意判断条件与置空

复制代码
linkqueue * queue_free(linkqueue *lq)
{

​    linklist p;





​    if(lq == NULL)

​    {

​        printf("lq is NULL\n");

​        return NULL;

​    }


​    while(lq->front)

​    {

​      p = lq->front;

​      lq->front = p->next;

​      printf("free:%d\n",p->data);

​      free(p);

​      p = NULL;

​    }

​    free(lq);

​    lq = NULL;

​    return lq;
}

假溢出:

什么是假溢出呢,首先我们先了解下什么是队列中的溢出/上溢,也就是当队列里的空间已经满了,还继续往队列中插入元素,就会使队列的基础数组越界而导致代码破坏;而假溢出则是由于头尾指针都不断地向前移因而超出向量空间,这时整个向量空间及队列是有空的地方却产生了"上溢"现象。

就像上图中的0和1 位置明明是空的,末端指针却指在数组外面因此加不进新的元素,就产生了数组越界的错误。

因此我们采用循环队列来防止假溢出的现象的发生。

链式、栈的综合应用--球钟问题

问题描述:

原理:

要解决的问题:

示意图如下:

分析:

三个时间指示器可看作三个栈容器,并且都是有最大存储容量的容器,因此考虑到顺序栈(maxlen),入栈出栈元素满足先进后出,球队列可以选择用更为方便的链式队列。

根据题意,三个栈容器均为空时,时间指示为0:0,当三个栈容器分别达到上限时,时间指示为11:59,最后再放一个球,时间归零,构成一个循环。故总的球链式队列里初始值应为27个球。

源码如下:

复制代码
int check(linkqueue *lq);

int main()

{

​    linkqueue *lq;

​    sqstack *s_hour,*s_five,*s_min;   //定义三个顺序栈

​    int i,min = 0;                    //定义初始时间

​    int value;





​    if((lq = queue_create()) == NULL)

​    {

​        return -1;

​    }





​    for(i=1;i<=27;i++)

​    {

​        enqueue(lq,i);                 //循环入队,设置初始元素队列

​    }









​    if((s_hour = sqstack_create(11)) == NULL)    //创建11容量的一小时栈

​    {

​        return -1;

​    }





​    if((s_five = sqstack_create(11)) == NULL)    //创建11容量的五分钟栈

​    {

​        return -1;

​    }





​    if((s_min = sqstack_create(4)) == NULL)   //创建4容量的一分钟栈

​    {

​        return -1;

​    }





​    while(1)                  

​    {

​        min++;                               //循环计时,每过一分钟就从队列中取出一个球

​        if(!queue_empty(lq))

​        {

​            value =  dequeue(lq);            //非空取球





​            if(!sqstack_full(s_min))         //未满元素压栈,即分钟指示器多一个球

​            {

​                sqstack_push(s_min,value);

​            }

​            else

​            {

​                while(!sqstack_empty(s_min))  //如果分钟指示器满了,再放入一个球,分钟指示器的4个球会出栈入队(与入栈顺序相反放入队尾),直到栈为空

​                {

​                    enqueue(lq,sqstack_pop(s_min));

​                }

​                if(!sqstack_full(s_five))     //如果这时5分钟指示器未满,则压栈

​                {

​                    sqstack_push(s_five,value);

​                }

​                else

​                {

​                    while(!sqstack_empty(s_five))//否则,视为5分钟指示器也满了,这时将5分钟指示器元素出栈入队,直到为空

​                    {

​                        enqueue(lq,sqstack_pop(s_five));

​                    }

​                    if(!sqstack_full(s_hour))   //判断小时指示器是否已满,未满则将这个球放入小时指示器

​                    {

​                        sqstack_push(s_hour,value);

​                    }

​                    else

​                    {

​                        while(!sqstack_empty(s_hour))  //否则,也即是小时指示器也满了,则出栈入队,直到栈为空

​                        {

​                          enqueue(lq,sqstack_pop(s_hour));

​                        }





​                        enqueue(lq,value);            //将最后未放入小时指示器的球置入队尾,此时达到一个循环     

​                        //0:0





​                        if(check(lq))                 //检查链式队列是否升序

​                        {

​                          break;                      //若升序,则跳出整个循环,计时结束

​                        } 

​                    }

​                }

​            }





​        }





​    }





​    printf("total:min=%d\n",min);









​    printf("dequeue:");

​    while(!queue_empty(lq))

​    {

​        printf("%d ",dequeue(lq));

​    }





​    puts("");





​    return 0;

}


int check(linkqueue *lq)//判断是否升序,前一个数与后一个数比较,若小于继续比较,否则退出,直到比完

{

​    linklist p;





​    if(lq == NULL)

​    {

​      return -1;

​    }


​    p = lq->front->next;

​    while(p && p->next)

​    {

​        if(p->data < p->next->data)

​        {

​           p = p->next;

​        }

​        else

​        {

​            return 0;

​        }

​    }


​    return 1;

}

树:

树的概念:

有序树和森林:

树的逻辑结构:

二叉树:

概念:

二叉树的性质:

性质一:二叉树第i层上的节点最多为2的(i-1)次方个(i>=1)

性质二:深度为k的二叉树节点数最多有2的k次方减一(k>=1)

性质三:具有n个节点的完全二叉树深度为:(log2n)+1 或者log2(n+1)

另外二叉树满足:度数为0的节点数等于度数为2的节点数加1

满二叉树:

深度为k时有2的k次方减一个节点

完全二叉树:

只有最下面两层度数小于2,且最下面一层的节点集中在最左边的若干位置上

完全二叉树的度数为1的结点只有两种情况,0或1

二叉树的顺序存储:

可以利用数组进行顺序存储,数组元素下标与二叉树的节点编号一一对应,下标0元素不用,但是这种有时会浪费存储空间,因为当遇到不完全二叉树时,需要添加虚节点,此时虚节点会占据存储空间

因此采用二叉树的链式存储结构:

结构体定义:

二叉树的遍历:

遍历:

为什么说二叉树是非线性的呢?

学习线性表时,知道线性表是表头无前驱,表尾无后继,其他节点有且仅有一个直接前驱和一个直接后继,像一串糖葫芦,是一串线性元素的序列,而对于二叉树,根节点后有多个后继节点时,这时就是树形结构,是一种非线性结构。

因为每一个节点有两个后继结点,因此存在着如何遍历的问题,每一种遍历的搜索路径不一样,遍历的结果也不一样,分情况进行遍历:

以先序遍历为例:

创建:

二叉树中的所有的字母均由用户输入(包括#号),#号代表接口函数的出口,返回NULL,图中所有的空操作全部用#号代替。

二叉树创建源码如下:

复制代码
bitree *tree_create()
{

   data_t ch;

   bitree *r;


   scanf("%c",&ch);

   if(ch != '#')

   {

​      if((r = (bitree *)malloc(sizeof(bitree))) == NULL)

​      {

​         printf("malloc failed\n");

​         return NULL;

​      }


​      r->data = ch;

​      r->left = tree_create();

​      r->right = tree_create();

   }

   else

   {

​       return NULL;
   }


   return r;

}

其中,先定义一个二叉树根节点指针,用来接收创建的二叉树结点内存空间返回的首地址(强制类型转换),创建好之后,给该节点赋予初值:data域和左右子树,左右子树分别再次递归调用二叉树创建函数,执行创建函数过程中遇到#跳出,最后返回根节点指针r

二叉树先序遍历源码:

每次递归执行时执行够花括号里的四句话,遇到出口条件逐层跳出,直至结束

复制代码
int preorder(bitree *r)  //二叉树的根节点,看作先序遍历起始的地方
{
  if(r == NULL)

  {
​    return 0;
  }


  printf("%c",r->data);

  preorder(r->left);           //递归调用先序遍历函数,遇到#跳出一层循环

  preorder(r->right);

  return 0;

}


类似,中序和后序的源码如下:

int inorder(bitree *r)

{
  if(r == NULL)

  {

   return 0;

  }

  inorder(r->left);

  printf("%c",r->data);

  inorder(r->right);


  return 0;

}


int postorder(bitree *r)

{

  if(r == NULL)

  {

   return 0;

  }


  postorder(r->left);

  postorder(r->right);

  printf("%c",r->data);


  return 0;

}

三种遍历示意图;

先序遍历算法剖析:(想不明白时,最好画出最简单的二叉树,将其按照递归算法捋一捋)

层次遍历:

声明一个队列,用于存放父节点,便于之后找左右子树,基本思路是:

访问打印根节点的数据,并使根节点入队。下面构建一个循环,结束条件是队列为空,然后将根节点出队,判断根节点的左子树是否为空,若不为空则访问左子树,访问完后将该左子树入队;若为空,再判断有无右子树,若右子树不为空,则访问右子树的数据,并将右子树节点掷入队列,接下来是又一层执行。队列不为空,将先前掷入队列的节点出队访问,判断有无左右子树...............

以上面的二叉树遍历图为例:先访问根节点A,再将A入队,接着进入循环语句,队列不为空时将A出队,判断左右子树,访问左右子树的数据,然后将左、右子树入队.........

根据以上的思路,则层次遍历的顺序为A-B-E-C-F-D-G-H-K

源码如下:

复制代码
int layerorder(bitree *r)

{

  linkqueue *lq;





  if((lq = queue_create()) == NULL)

  {

​    return -1;

  }





  if(r == NULL)

  {

​     return 0;

  }





  printf("%c",r->data);

  enqueue(lq,r);





  while(!queue_empty(lq))

  {

​      r = dequeue(lq);

​      if(r->left)

​      {

​        printf("%c",r->left->data);

​        enqueue(lq,r->left);

​      }





​      if(r->right)

​      {

​        printf("%c",r->right->data);

​        enqueue(lq,r->right);





​      }





  }

  puts("");





  return 0;

}

防重复定义(包含)

曾出现这样的问题:矛盾类型、重复定义

文件包含情况如下:

观察可发现,bitree.h与linkqueue.h头文件均被包含在bitree.c文件中,另外我在这两个头文件中均加上了条件编译,可为什么还会报这样的错误呢?

细看可发现,bitree.h与linkqueue.h头文件中定义的类型有一样的、重复的(比如:data_t,node)这样的话,预处理阶段进行头文件展开生成.i文件,会将bitree.h先展开,再将linkqueue.h展开,而linkqueue.h文件中又包含了bitree.h,于是又将bitree.h在bitree.c文件中展开,最后将linkqueue.h中剩下内容展开,生成最终的.i文件。

于是,bitree.h包含了两次,linkqueue.h包含了一次,此外bitree.h与linkqueue.h都进行了条件编译,因此编译时只将bitree.h与linkqueue.h各编译一次。但是重点是,即使这样,bitree.h与linkqueue.h因有重复内容,编译时也会出错

现在改为如下:将bitree.h文件中的data_t、node改为datatype、node_t

再次编译:正常

此外将bitree.c中的bitree.h注释掉也可以,因为bitree.h已经包含在了linkqueue.h,这样包含了linkqueue.h的情况下,会将bitree.h与linkqueue.h各包含一次,编译时也均编译一次

但是注意:如果将linkqueue.h文件中的#include "bitree.h"注释掉,编译时会出现以下错误:

虽然看上去和上面的类似,也都各包含一次,但是由于没有包含到linkqueue.h文件中,就会出现未知类型、找不到相关类型的错误

查找:

查找的概念:

查找的方法:

自己去实现顺序表查找与折半查找

查找的平均长度:

哈希查找法:

哈希查找可以不经过比较就可以查找到相应的记录,其时间复杂度为常数级O(C),需要建立记录与地址的链接关系

哈希函数(散列函数、杂凑函数):

通常要找Key = k的记录时,其中k为用户要查找的数,通过建立哈希函数确定记录所在的地址,要查找时,先定位到地址,再进行查找访问,H(key)记为哈希函数,自变量key是记录,函数值为存储地址或哈希地址。

冲突:

对于同一个哈希函数,不同的key可能对应同一个地址,这时称两个记录为同义词,该现象为"冲突"

事实上,选取好的哈希函数只能尽量使冲突减少,却不能完全避免冲突现象的发生,这就需要考虑到冲突数据的存放问题

哈希表:

将一组记录(线性表),通过某种映射关系(哈希函数与解决冲突方法)映射存放到一个存储空间中,形成一个记录表,该记录表称为哈希表

哈希表的查找:

哈希函数的构造有多种方法:直接地址法、平方取中法、叠加法、保留余数法、随机函数法,主要介绍最常用的保留余数法

保留余数法:

H(key)= key % p ,p为不大于哈希表长m的最大质数

p选取质数比合数的哈希函数的随机度要好,散列程度要优,可以使得冲突现象减少

使用随机度好的哈希函数可以使得冲突减少,但不能完全避免冲突现象的发生,也就是说冲突现象可能再哈希表中发生,例如根据哈希函数得到哈希地址,不同的记录可能对应多个哈希地址,一个地址内存只能存放一个记录,这就导致了冲突现象的发生

解决冲突的方法:

哈希表的装填因子一般选取经验值0.7-0.8之间,根据记录数推算出表长m,视为哈希表长,另外表中留有余量,减少装填数据造成的冲突和聚积

开放地址法:

示意图:

链地址法:

有这样一组记录:key={23,34,14,38,46,16,68,15,7,31,26},共11个记录数据,根据前面的内容,装填因子 = n/m,选取装填因子为0.75,可推算出m=15,根据哈希函数H(key) = key % p,选取最大质数为13,即哈希表长为15,余数空间为【0,12】,也就是说记录如果分散到哈希表中,数据以链式存储的方式分布于哈希地址为【0,12】的空间。

示意图:

结构体定义:

首先定义一个链式节点listnode,成员分别为key、value、next,再将其扩充封装为一个链式节点数组,定义为hash,数组中每一个元素都是一个链式节点

复制代码
#ifndef __HASH_H__

#define __HASH_H__





#define N 13

typedef int datatype;


typedef struct node

{

  datatype key;

  datatype value;

  struct node *next;

}listnode,*linklist;



typedef struct

{

  listnode data[N];

}hash;


hash *hash_create();

int hash_insert(hash *HT,datatype key);

linklist hash_search(hash *HT,datatype key);


#endif

然后定义一个hash指针HT(hash *HT)指向这一片链式节点结构体数组,便可用HT指针进行记录结点的插入和查找操作,类似于链表中的头指针

创建:

复制代码
hash *hash_create()

{

​    hash *HT;

   

​    if((HT = (hash *)malloc(sizeof(hash))) == NULL)

​    {

​        printf("malloc failed\n");

​        return NULL;

​    }





​    memset(HT,0,sizeof(hash));





​    return HT;

}

定义一个HT指针,用于接收申请内存返回的首地址,若申请失败返回NULL,成功进行初始化赋值,调用memset函数填充

插入:(有序插入)

复制代码
int hash_insert(hash *HT,datatype key)

{

   

   linklist p,q;





   if(HT == NULL)

   {

​      return -1;

   }





   if((p = (linklist)malloc(sizeof(listnode))) == NULL)

   {

​      printf("malloc listnode failed\n ");

​      return -1;

   }





   p->key = key;

   p->value = (key % N);

   p->next = NULL;





   q = &(HT->data[key % N]);





   while(q->next && q->next->key < p->key)  //退出条件为q的指针域为空,此时在后面追加   ,另外如果q指向的下一个结点的记录不小于要插入的记录,那就退出循环,在两节点中间插入

   {

​      q = q->next;

   }





   p->next = q->next;

   q->next = p;





   return 0;

}

先定义一个链式指针p,用于接收申请内存后返回的首地址,p指向该内存空间,接着对p所指向的内存,进行初始化赋值,其中key为记录,value为保留余数法求出的哈希地址,next为指向下一节点的指针域。

初始化赋值后选择要插入的那一串链表,即要先找到其头结点,这里用 q = &(HT->data[key % N]),以哈希地址为下标的数组元素就是对应头结点,然后加上7取地址,赋给指针变量q,q为该串链表的头结点指针,然后进行插入操作(循环出口为q指针域为空或要插入的值不大于q指针的下一结点的记录)

查找:

复制代码
linklist hash_search(hash *HT,datatype key)

{

   linklist p;





   if(HT == NULL)

   {

​      return NULL;

   }





   p = &(HT->data[key % N]);





   while(p->next && p->next->key != key)

   {

​      p = p->next;

   }





   if(p->next == NULL)

   {

​     printf("not found:\n");

​     return NULL;

   }





   else

   {

​      printf("found:\n");

​      return p->next;

   }

}

类似于插入操作,先找到所在链表的头结点,如果p的指针域为空或者p指向的下一个节点的记录等于用户要查找的值,接着进行判断输出对应的结果


排序:

排序定义:

内排序方法:

而插入排序又可分为:

(1)直接插入排序:

(2)折半插入排序:

(3)链表插入排序:

交换排序:

①:冒泡排序(自己去实现,一定要熟练,本节不予实现)

②:快速排序:(非常重要,一定要熟练理解)

快速排序:


快排基本思路:(递归)

给一个数组或者记录集合,先找一个基准值(第一个元素),并且用一第三变量存起来。默认基准值右边的数据比基准值大、左边的数据比基准值小,以此为规则进行比较,从右边开始比较,基准值右边比基准值大的数不用动,high--(记录两个游标low、high),若遇到基准值右边比基准值小的数,则将该数放到基准值处,方便理解可以认为基准值虚假占位(实际上基准值已经被存放到第三变量中,不用进行交换,此后开始正序比较,遇到左边比基准值大的数,数据送到刚刚反序截止的high位,该位置被基准值虚假占据),以此类推,第一次执行到退出条件时,找到一个中间轴,数据被分为两部分,分成的两部分再次调用快排函数(递归),反复调用执行,直到结束

复制代码
#include <stdio.h>

#include <stdlib.h>

#define N 15



//int compare(const void *p1,const void *p2);

int partion(int *data,int low,int high);

int quicksort(int *data,int low,int high);

int main()

{
​    int data[N] = {0};

​    int i;


​    srandom(9);  //随机值函数


​    for(i=0;i<N;i++)

​    {

​       data[i] = random() % 100;

​    }


​    for(i=0;i<N;i++)

​    {

​       printf("%d ",data[i]);

​    }

​    puts("");


​    quicksort(data,0,N-1);

​    //qsort(data,N,sizeof(int),compare);//库函数快排

​    for(i=0;i<N;i++)

​    {

​       printf("%d ",data[i]);

​    }

​    puts("");


​    return 0;

}


int partion(int *data,int low,int high)

{

​    int tem = data[low];


​    while(low < high)

​    {

​      while(low < high && tem <= data[high])    //如果碰到极端情况基准值右边的数都比基准值大,循环会被一直执行,即使low和high相等high还在减,大括号不能对其控制,故应把大括号的退出条件加到内层循环中

​      {

​          high--;

​      }


​      data[low] = data[high];


​      while(low < high && tem >= data[low])    //同上

​      {

​        low++;

​      }


​      data[high] = data[low];

​    }


​    data[low] = tem;


​    return low;

}

int quicksort(int *data,int low,int high)

{
​    int t;


​    if(data == NULL)

​    {

​      return -1;

​    }


​    if(low >= high)            //为什么这里写为 low ==  high  同目录下的作业文件quicksort1c会报段错误

​    {

​      return 0;

​    }


​    t = partion(data,low,high);


​    quicksort(data,low,t-1);

​    quicksort(data,t+1,high);


​    return 0;
}


/*int compare(const void *p1,const void *p2)

{
​    return (*(const int *)p1 - *(const int *)p2);
}*/

如果low == high:

具体见错题集

使用库函数快排:

各种排序算法详解

多层循环编程经验:

做一件事一般要分成两步,第一步先确定分几步进行,为最外层循环,第二步确定每一大步里面的每一小步该怎么实现,为内层循环

①:冒泡排序:

复制代码
#include <stdio.h>



int main()

{

​    int i,j;

​    int t,n;





​    int data[] = {1,5,3,8,6,2,0};

​    n = sizeof(data)/sizeof(int);





​    for(i=0;i<n;i++)

​    {

​       printf("%d ",data[i]);

​    }

​    puts("");





​    for(i=0;i<n-1;i++)

​    {

​        int len = n;

​        for(j=0;j<len-1;j++)

​        {

​            if(data[j]>data[j+1])

​            {

​               t = data[j];

​               data[j] = data[j+1];

​               data[j+1] = t;

​            }

​        }





​        len--;

​    }



​    for(i=0;i<n;i++)

​    {

​       printf("%d ",data[i]);

​    }

​    puts("");

​    return 0;

}

②:简单插入排序:

思路:

例如对于一组无序数据{3,2,5,4,11,15,13},第0个数据为基准值下标为end,从第1个数据开始,先用第三变量存储,再和前一个数据进行比较,如果比前一个数据小,那就end--,直到碰到比该数据大的跳出循环,将先前保存的值插入到该位置的后一个位置,如果所比较的所有数都比该数据大,那么当end=-1时直接结束循环,该数据直接赋予a[0]

复制代码
#include <stdio.h>



int main()

{  

​    int a[] = {3,2,5,4,11,15,13};

​    int i,end;

​    int n,t;

​    

​    n = sizeof(a)/sizeof(int);



​    for(i=0;i<n-1;i++)

​    {

​       end = i;

​       t = a[end+1];



​       while(end >= 0)

​       {

​         if(t < a[end])

​         {

​            a[end+1] = a[end];

​            end--;

​         }

​         else

​         {

​             break;

​         }

​       }

​       a[end+1] = t;

​    }

​    for(i=0;i<n;i++)

​    {

​        printf("%d ",a[i]);

​    }



​    puts("");

​    return 0;

}

③:希尔排序:

思路:

例如对于一个数组{9,1,2,5,7,4,8,6,3,5},希尔排序的思想为将数组先分成若干组,对每一组进行直接插入排序(增量为d1,程序里为gap=n/2),然后对新数组再分为若干组,在对每一组进行直接插入排序(增量为d2<d1),以此类推,直到dt=1时结束排序,而且发现dt=1即为简单插入排序

下面取gap为每一循环增量减半:

复制代码
#include <stdio.h>


int main()

{

​    int a[] = {9,1,2,5,7,4,8,6,3,5};

​    int i,end,gap;

​    int n,t;

​    n = sizeof(a)/sizeof(int);



​    gap = n/2;

​    while(gap >= 1)

​    {

​        for(i=0;i<n-gap;i++)

​        {

​            end = i;

​            while(end >= i)

​            {

​                

​                t = a[end + gap];

​                if(t < a[end])

​                {

​                    a[end + gap] = a[end];

​                    end--;

​                }

​                else

​                {

​                    break;

​                }

​            }

​            if(end < i)

​            {

​               a[end + 1] = t;

​            }

​        }

​        gap /= 2;

​    }


​    for(i=0;i<n;i++)

​    {

​       printf("%d ",a[i]);

​    }


​    puts("");

​    return 0;

}

④:选择排序:

思路:

首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。

选择排序的思想其实和冒泡排序有点类似,都是在一次排序后把最小的元素放到最前面,或者将最大值放在最后面。但是过程不同,冒泡排序是通过相邻的比较和交换。而选择排序是通过对整体的选择,每一趟从前往后查找出无序区最小值,将最小值交换至无序区最前面的位置。

复制代码
#include <stdio.h>


int main(int argc,char *argv[])

{

​    int a[10] = {9,1,2,5,7,4,8,6,3,5};

​    int b[10] = {0};

​    int i=0,j,r=1;

​    int min,t;


​    for(j=0;j<10;j++)

​    {

​        r = i;

​        while(r<=9)

​        {

​            if(a[i] > a[r])

​            {

​                t = a[i];

​                a[i] = a[r];

​                a[r] = t;

​                min = a[i];

​            }

​            r++;

​        }    

​      
​        b[j] = min;

​        i++;

​    }

​    for(j=0;j<10;j++)

​    {

​       printf("%d ",b[j]);

​    }


​    puts("");

​    return 0;
}

⑤:归并排序:

相关推荐
XSTIT3 分钟前
数据结构--二叉树相关题2(OJ)
数据结构·算法
续亮~10 分钟前
6、Redis系统-数据结构-06-跳表
java·数据结构·数据库·redis·后端·缓存
郝YH是人间理想24 分钟前
《算法笔记》总结No.3——排序
c语言·数据结构·c++·算法·排序算法·csp
a_golden_fish24 分钟前
【做一道算一道】滑动窗口最大值
数据结构·算法
creative_mind37 分钟前
优选算法之技巧(一):双指针一:移位0与复写0
数据结构·算法·优选算法
续亮~2 小时前
6、Redis系统-数据结构-07-QuickList
数据结构·数据库·redis
ToBeWhatYouWannaBe.3 小时前
代码随想录-Day49
java·数据结构·算法·leetcode
Little Tian3 小时前
插入排序——C语言
c语言·数据结构·算法·排序算法
韩楚风3 小时前
【手写数据库内核组件】0201 哈希表hashtable的实战演练,多种非加密算法,hash桶的冲突处理,查找插入删除操作的代码实现
c语言·数据结构·数据库·哈希算法·散列表
续亮~3 小时前
9、Redis 高级数据结构 HyperLogLog 和事务
数据结构·数据库·redis