数据结构学习路线+笔记

来源:互联网 发布:php session redis 编辑:程序博客网 时间:2024/06/03 19:51
在学校学习过后感觉数据结构学完之后还是迷迷糊糊的,而数据结构已经有很多位前辈说过其重要性,于是下决心再来一遍“复习”。代码会陆续贴上来,轻喷,都是些简单的基础知识。
————学习来源为郝斌老师的视频
数据结构:数据结构是研究数据存储的问题(狭义)数据结构既包含数据的存储,也包含对存储数据的操作即算法(广义)
数据存储包含两个方面: 个体的存储 + 个体关系的存储。

算法:
狭义的算法是与数据的存储方式密切相关的
广义的算法是与数据的存储方式无关,
泛型:利用某种技术达到的效果就是不同的存储方式执行操作是一样的。

数据的存储结构:线性和非线性

线性结构:任何一个节点只能对应一个节点,即所有节点都由一根“线”连接。
连续存储【数组】
优点:存取速度快,
缺点:必须先知道数组长度,插入删除元素操作速度慢,空间通常是有限制的,需要极大的连续内存块。

离散存储【链表】
定义:N个节点离散分配,彼此通过指针指向下一个节点,每个节点只有一个前驱节点和一个后继节点,首节点没有前驱,尾节点没有后继节点。

优点:插入删除速度快,空间没有限制(容量)
缺点:存取速度慢

专业术语:
首节点
尾节点
头节点:在首节点(有效节点)的前面添加的节点为头节点,其没有实际含义,没有有效数据,其存在的意义是简便对链表的操作(算法)。
头指针:指向头节点的指针变量,存放头节点的指针地址
尾指针:指向尾节点的指针
如果希望通过函数来对一个链表进行处理,我们至少需要接收一个参数:头指针,通过头指针可以推算出链表的所有信息

分类:
单链表:每个节点只有一个指针域,指向下一个节点
双链表:每一个节点有两个指针域,分别指向前驱和后继
循环链表:任何一个节点都可以找到其他所有节点,最后一个节点指向第一个指针域。
非循环链表

相关算法:
遍历:
查找:
清空:
销毁:
求长度:
排序:
删除节点:
插入节点:
#define _CRT_SECURE_NO_DEPRECATE
#include <stdio.h>
#include <malloc.h>
#include <stdlib.h>


typedef struct Node
{
int data;
struct Node * pNext;
}NODE, *PNODE;




PNODE create_list(void);//创建一个单链表
void traverse_list(PNODE pHead);//遍历一个链表
bool is_empty(PNODE pHead);
int getLength_lsit(PNODE pHead);
bool insert_list(PNODE pHead, int pos, int value);
bool delete_list(PNODE pHead, int pos, int *pValue);
void sort_list(PNODE pHead);


int main()
{
PNODE pHead = NULL;
pHead = create_list();
getchar();
traverse_list(pHead);
int len = getLength_lsit(pHead);
printf("长度:%d\n", len);


sort_list(pHead);
traverse_list(pHead);
system("pause");
return 0;
}


PNODE create_list(void)
{
int len;//有效节点个数
int i;
int value;//临时数据


//不存放有效数据的头节点
PNODE pHead = (PNODE)malloc(sizeof(NODE)); //为生成的节点动态分配内存
if (NULL == pHead)
{
printf("节点生成失败,程序终止!");
exit(-1);
}
PNODE pTail = pHead;
pTail->pNext = NULL;//清空,指向最后一个节点


printf("输入有效节点个数len:");
scanf("%d", &len);


for (i = 0; i < len; i++)
{
printf("请输入第%d个几点值:",i+1);
scanf("%d", &value);


PNODE pNew = (PNODE)malloc(sizeof(NODE));
if (NULL == pNew)
{
printf("节点生成失败,程序终止!");
exit(-1);
}
pNew->data = value;
pTail->pNext = pNew;
pNew->pNext = NULL;//最后一个节点的指针域为空
pTail = pNew;
}
return pHead;

}


void traverse_list(PNODE pHead)
{

PNODE p = pHead->pNext;
while (NULL != p)
{
printf("%d ",p->data);
p = p->pNext;
}
printf("\n");
return;
}


bool is_empty(PNODE pHead)
{
if (pHead->pNext == NULL)
{
return true;
}
else
{
return false;
}
}


int getLength_lsit(PNODE pHead)
{
int cnt = 0;
PNODE p = pHead->pNext;
while (NULL != p)
{
++cnt;
p = p->pNext;
}
return cnt;
}


void sort_list(PNODE pHead) //冒排
{
int i, j, t;
int len = getLength_lsit(pHead);
PNODE p, q;
for (i=0, p = pHead->pNext; i<len-1; i++,p = p->pNext)
{
for (j=i+1, q = p->pNext; j<len; j++,q=q->pNext)
{
if (p->data > q->data)
{
t = p->data;
p->data = q->data;
q->data = t;
}
}
}
return;
}


//在pHead所指向链表的第pos节点的前面插入一个新节点,pos从1开始
bool insert_list(PNODE pHead, int pos, int value)
{
int i = 0;
PNODE p = pHead;


while (NULL != p && i < pos - 1)
{
p = p->pNext;
++i;
}


if (i>pos - 1 || NULL == p )
{
return false;
}


PNODE pNew = (PNODE)malloc(sizeof(NODE));//开辟空间
if (NULL == pNew)
{
printf("动态分配内存失败!");
exit(-1);
}
pNew->data = value;
PNODE q = p->pNext;
p->pNext = pNew;
pNew->pNext = q;


return true;
}


bool delete_list(PNODE pHead, int pos, int *pValue)
{
int i = 0;
PNODE p = pHead;


while (NULL != p->pNext && i < pos - 1)
{
p = p->pNext;
++i;
}


if (i>pos - 1 || NULL == p->pNext)
{
return false;
}




PNODE q = p->pNext;
*pValue = q->data; //删除前把当前值保存
p->pNext = p->pNext->pNext;
free(q);
q = NULL;


return true;
}





静态分配内存处于栈中, 动态分配内存处于堆中


头:Top       尾:Bottom
线性结构的应用--栈
定义:一种可以实现先进后出的存储结构。

分类:
静态栈
动态栈

算法:出栈,压栈(入栈)

#include <stdio.h>
#include <stdlib.h>
#include <malloc.h>

typedef struct Node
{
int data;
struct Node * pNext;
}NODE, *PNODE;

typedef struct Stack
{
PNODE pTop;
PNODE pBottom;
}STACK, *PSTACK;

void init(PSTACK pSTACK);//初始化栈
void push(PSTACK pSTACK, int value);
void traverse(PSTACK pSTACK);
bool pop(PSTACK pSTACK, int *pTemp);
void clear(PSTACK pSTACK);

int main(void)
{
int temp;
STACK S;
init(&S); //初始化一个栈,空栈
push(&S, 100);//压栈
push(&S, 200);
push(&S, 300);
push(&S, 400);
push(&S, 500);
traverse(&S);//遍历输出

if (pop(&S, &temp))
{
printf("出栈成功,元素是:%d\n",temp);
}
else
{
printf("出栈失败");
}

traverse(&S);
clear(&S);
printf("清空后:");
traverse(&S);

system("pause");


return 0;
}

void init(PSTACK pSTACK)
{
pSTACK->pTop = (PNODE)malloc(sizeof(NODE));
if (NULL == pSTACK->pTop)
{
printf("动态内存分配失败");
exit(-1);
}
else
{
pSTACK->pBottom = pSTACK->pTop;
pSTACK->pTop->pNext = NULL;
}

}


void push(PSTACK pSTACK, int value)
{
PNODE pNew = (PNODE)malloc(sizeof(NODE));
pNew->data = value;
pNew->pNext = pSTACK->pTop;
pSTACK->pTop = pNew;

return;
}


void traverse(PSTACK pSTACK)
{
PNODE p = pSTACK->pTop;


while (p != pSTACK->pBottom)
{
printf(" %d", p->data);
p = p->pNext;
}
printf("\n");
return;
}


bool empty(PSTACK pSTACK) //判断栈是否为空
{
if (pSTACK->pTop == pSTACK->pBottom)
{
return true;
}
else
{
return false;
}
}


//将pStack栈执行一次出栈,并把出栈的值存入形参pTemp所指向的变量中,成功true,失败false
bool pop(PSTACK pSTACK, int *pTemp)
{
if (empty(pSTACK))  //pSTACK本身存放的就是S的地址
{
return false;
}
else
{
PNODE r = pSTACK->pTop;
*pTemp = r->data;
pSTACK->pTop = r->pNext;
free(r);
r = NULL;


return true;
}
}


void clear(PSTACK pSTACK)  //清空
{
if (empty(pSTACK))
{
printf("栈已空");
}
else
{
PNODE p = pSTACK->pTop;
PNODE q = NULL;
while (p != pSTACK->pBottom)
{
q = p->pNext;
free(p);
p = q;
}
pSTACK->pTop = pSTACK->pBottom;


}
}


应用:
函数调用
中断
表达式求值(通过两个栈求值,一个栈存放值,一个栈存放运算符)
内存分配
缓冲处理
迷宫

线性结构的应用--队列
定义:一种可以实现“先进先出”的存储结构

头:front        尾:rear

分类:
链式队列(链表实现的队列)
静态队列(数组实现的队列)
静态队列通常必须是循环队列

循环队列:
1、静态队列为什么必须是循环队列
防止指针溢出,在队列中,无论是添加还是删除元素,front指针和rear指针都是向上加一位,所以设置循环,在指针达到峰值后循环到最低位继续加。

2、循环队列需要几个参数来确定及其含义
    需要两个参数来确定,两个参数在不同场合有不同的定义
    分别是front和rear

3、循环队列各个参数的含义
    1)初始化
        front和rear的值都是零
    2)队列非空
        front代表队列的第一个元素,rear代表队列的最后一个有效元素的下一个元素
    3)队列空
        front和rear的值相等,但不一定为零

4、循环队列入队伪算法
    1)将值存入rear指向的位置
    2)rear指针不能直接加一(后移),应该写成rear = (r+1)%数组长度

5、循环队列出队伪算法
    1)front = (front+1)%数组长度
    2)(原front指向的值可以存,也可以不存)

6、如何判断循环队列是否为空
    两个参数值相等时,队列为空,即front = rear

7、如何判断循环队列已满
    front和rear的值是不确定的,大小关系也是不确定的,毫无规律可循。
    
    两种方式:1、多增加一个标志位参数(此方式相对麻烦,每次都需要更改参数)
                      2、少用一个元素,如果front和rear相邻,则队列已满
    通常使用第二种方式 (伪算法表示)
        if (   (r+1) %数组长度 == f   )
            队列满
        else
            队列不满



算法:出队、入队

#include <stdio.h>
#include <malloc.h>
#include <stdlib.h>


typedef struct Queue //队列的结构体
{
int *pBase;
int front;
int rear;
}QUEUE;


void init(QUEUE *pQ);
bool en_queue(QUEUE *pQ, int value);
void traverse_queue(QUEUE *pQ);
bool full_queue(QUEUE *pQ);
bool out_queue(QUEUE *pQ, int *pValue);
bool empty_queue(QUEUE *pQ);


int main(void)
{
QUEUE Q;
int value;


init(&Q);
en_queue(&Q, 1);
en_queue(&Q, 2);
en_queue(&Q, 3);
en_queue(&Q, 4);
en_queue(&Q, 5);
en_queue(&Q, 6);


traverse_queue(&Q);


if (out_queue(&Q, &value))
{
printf("出队成功,元素:%d\n",value);
}
else
{
printf("出队失败");
}
traverse_queue(&Q);


system("pause");
return 0;
}


/*
初始化队列
front和rear都初始化为0,初始化数据域
*/
void init(QUEUE *pQ) 
{
pQ->pBase = (int *)malloc(sizeof(int) * 6);
pQ->front = 0;
pQ->rear = 0;
}


/*
入队
*/
bool en_queue(QUEUE *pQ, int value)
{
if (full_queue(pQ))
{
return false;
}
else
{
pQ->pBase[pQ->rear] = value;//将值存入队列顶部即rear所指向的地方
pQ->rear = (pQ->rear + 1) % 6; //rear向后移位(循环队列)
return true;
}
}






bool full_queue(QUEUE *pQ)
{
if ((pQ->rear + 1) % 6 == pQ->front)
return true;
else
return false;
}


void traverse_queue(QUEUE *pQ)
{
int i = pQ->front;
while (i != pQ->rear)
{
printf("%d ",pQ->pBase[i]);
i = (i + 1) % 6;
}
printf("\n");


return;
}


bool empty_queue(QUEUE *pQ)
{
if (pQ->front == pQ->rear)
return true;
else
return false;
}


bool out_queue(QUEUE *pQ, int *pValue)
{
if (empty_queue(pQ))
{
return false;
}
else
{
*pValue = pQ->pBase[pQ->front];
pQ->front = (pQ->front + 1) % 6;
return true;
}
}

队列的应用----所有和时间有关的操作都与队列有关


专题:递归
定义:一个函数自己直接或间接调用自己

递归要满足的三个条件:
1、递归必须有一个明确的终止条件
2、该函数处理的数据规模必须在递减
3、这个转化必须是可解的

举例:
1、求阶乘
2、1+2+。。。+100的和
3、汉诺塔
4、走迷宫

循环和递归
        所有的循环都可以用递归实现,但是所有的递归不一定能用循环实现。

    递归:
           易于理解,
            速度慢,存储空间大

    循环:
            不易理解
            速度快,存储空间小


当一个函数的运行期间调用另一个函数时,在运行被调函数之前,系统需要完成三件事:
1、将所有的实际参数,返回地址等信息传递给被调函数保存。
2、为被调函数的局部变量(也包括形参)分配存储空间。
3、将控制转移到被调函数入口。

从被调函数返回主函数之前,系统需要完成三件事:
1、保存被调函数的返回结果
2、释放被调函数所占存储空间
3、依照被调函数保存的返回地址将控制转移到调用函数

当有多个函数相互调用时,按照“后调用先返回”的原则,上述函数之间信息传递和控制转移必须借助“栈”来实现,即系统将整个程序运行时所需的数据控件安排在一个栈中,每当调用一个函数时,就在栈顶分配一个存储区,进行压栈操作,每当一个函数退出时,就释放他的存储区,进行出栈操作,当前运行的函数永远在栈顶位置。

汉诺塔
if(n>1)
{
    先把A柱子上的前n-1个盘子从A借助C移到B
    将A柱子上的第n个盘子直接移到C
    再将B柱子上的n-1个盘子借助A移到C
}

#include <stdio.h>
#include <stdlib.h>
void hanor(int n, char x, char y, char z);
int main(void)
{
char ch1 = 'A'; //ABC分别代表柱子
char ch2 = 'B';
char ch3 = 'C';
int n; 
printf("请输入要移动的盘子数量");
scanf("%d", &n);
hanor(n,'A','B','C');
system("pause");
return 0;
}
/*n个盘子,从A借助B移到C*/
void hanor(int n, char a, char b, char c)
{
if (1 == n)
{
printf("将编号为%d的盘子直接从%c柱子移到%c柱子上\n", n, a, c);
}
else
{
hanor(n - 1, a, c, b);
printf("将编号为%d的盘子直接从%c柱子移到%c柱子\n", n,a,c);
hanor(n - 1, b, a, c);
}
}

递归的应用:
树和森林就是以递归的方式定义的
树和图的很多算法都是以递归的方式来实现的
很多数学公式就是以递归的方式定义的


逻辑结构
    线性
        数组
        链表
            栈和队列是一种特殊的线性结构    

    非线性
        树
        

物理结构
  

非线性结构:
    
        定义:有且只有一个根节点,有若干个互不相交的子树,子树本身也是一颗               树。
                   树是由节点和边组成,边就是指针域,存放着下一个节点的地址,每一个节点只有一个父节点,但可以有很多个子节点。 有一个节点没有父节点,为根节点。

    专业名词:节点、父节点、子节点、子孙、堂兄弟(父节点是兄弟节点)、深度(从根节点到最底层节点的层数,根节点是第一层)、叶子节点(没有子节点)、非终端节点(非叶子节点)、度(子节点个数)

        分类:
                一般树:任意一个节点的子节点个数都不受限制
                二叉树:任意一个节点的子节点个数最多有两个,且子节点的位置不可更改
                    分类:一般二叉树、满二叉树(在不增加二叉树深度的时候,节点数目饱和)、完全二叉树(满二叉树是完全二叉树的一个特例)
                森林:N个互不相交的树的集合


        存储:
                二叉树存储
                        连续存储(完全二叉树)

                        链式存储


                一般树的存储

                森林的存储
        操作:
  遍历
                先序遍历(先访问根节点)
                        先访问根节点
                        再先序访问左子树
                        再先序访问右子树    

                中序遍历(中间访问根节点)
                        中序遍历左子树
                        再访问根节点
                        再中序遍历右子树

                后序遍历(最后访问根节点)
                        中序遍历左子树
                        中序遍历右子树
                        最后访问根节点

                已知两种遍历求原始二叉树
                        通过先序和中序 或者 中序和后序 可以推算出原始二叉树
                        先序和后序无法还原原始二叉树    

        应用: 1、树是数据库中数据组织一种重要形式
                    2、操作系统子父进程的关系本身就是一棵树
                    3、面向对象语言中类的继承关系
                    4、赫夫曼树

二叉树例程
#include <stdio.h>
#include <stdlib.h>
#include <malloc.h>
/*下图为本程序静态二叉树
-----------A-------------
---------/---\-----------
--------B-----C----------
-------------/-----------
------------D------------
-------------\-----------
--------------E----------
*/
struct BTNode
{
char data;
struct BTNode *pLchild; //左右孩子节点
struct BTNode *pRchild;
};


struct BTNode * CreateBTree(void);//创建节点
void PreTraverseBTree(struct BTNode *pT);//先序遍历
void InTraverseBTree(struct BTNode *pT);//中序遍历
void PostTraverseBTree(struct BTNode *pT);//后序遍历


int main(void)
{
struct BTNode *pT = CreateBTree();
//printf("%c\n",pT->data);
//PreTraverseBTree(pT);
//InTraverseBTree(pT);
PostTraverseBTree(pT);



system("pause");
return 0;
}


struct BTNode * CreateBTree(void)
{
struct BTNode * pA = (struct BTNode *)malloc(sizeof(struct BTNode));
struct BTNode * pB = (struct BTNode *)malloc(sizeof(struct BTNode));
struct BTNode * pC = (struct BTNode *)malloc(sizeof(struct BTNode));
struct BTNode * pD = (struct BTNode *)malloc(sizeof(struct BTNode));
struct BTNode * pE = (struct BTNode *)malloc(sizeof(struct BTNode));


pA->data = 'A';
pB->data = 'B';
pC->data = 'C';
pD->data = 'D';
pE->data = 'E';


pA->pLchild = pB;
pA->pRchild = pC;
pB->pLchild = pB->pRchild = NULL;
pC->pLchild = pD;
pC->pRchild = NULL;
pD->pLchild = NULL;
pD->pRchild = pE;
pE->pRchild = pE->pRchild = NULL;


return pA;
}
/*
顺序:根节点->先序左子树->先序右子树
pT->pLchild代表左子树
pT->pRchild代表右子树
*/
void PreTraverseBTree(struct BTNode * pT)
{
if (NULL != pT)
{
printf("%c\n", pT->data);


if (NULL != pT->pLchild)
{
PreTraverseBTree(pT->pLchild);
}
if (NULL != pT->pRchild)
{
PreTraverseBTree(pT->pRchild);
}
}

}

//顺序:先序左子树->根节点->先序右子树
void InTraverseBTree(struct BTNode *pT)
{
if (NULL != pT)
{


if (NULL != pT->pLchild)
{
InTraverseBTree(pT->pLchild);
}


printf("%c\n", pT->data);


if (NULL != pT->pRchild)
{
InTraverseBTree(pT->pRchild);
}
}
}


//顺序:左子树->右子树->根节点
void PostTraverseBTree(struct BTNode *pT)
{
if (NULL != pT)
{
if (NULL != pT->pLchild)
{
PostTraverseBTree(pT->pLchild);
}
if (NULL != pT->pRchild)
{
PostTraverseBTree(pT->pRchild);
}
printf("%c\n", pT->data);
}
}




查找和排序

折半查找

排序:
        冒泡
        插入
        选择
        快速排序
        归并排序


快速排序例程
#include <stdio.h>
#include <stdlib.h>


void QuickSort(int * a, int low, int high);
int FindPos(int *a, int low, int high);


int main(void)
{
int a[6] = {3,6,2,7,1,4};
int i;
QuickSort(a,0,5);


for (i = 0; i < 6; ++i)
{
printf("%d ", a[i]);
}
printf("\n");


system("pause");


return 0;
}


/*
* a:存放值
low:第一个元素下标
high:最后一个元素下标
*/
void QuickSort(int * a, int low, int high)
{
int pos;//位置


if (low < high)
{
pos = FindPos(a, low, high);
QuickSort(a, low, pos - 1);
QuickSort(a, pos + 1, high);
}
}


//查找确定元素的位置
int FindPos(int *a, int low, int high)
{
int val = a[low];
while (low < high)
{
while (low < high && a[high] >= val)
{
--high;
}
a[low] = a[high];
while (low < high && a[low] <= val)
{
++low;
}
a[high] = a[low];
}
//终止while循环之后,low和high是相等的


a[low] = val;


return low;//返回位置信息
}

排序和查找的关系:排序是查找的前提,排序是重点

结论:数据结构是研究数据的存储和数据的操作的一门学问
            数据的存储分为两部分:
                    个体的存储
                    个体关系的存储
                    从某个角度而言,数据的存储最核心的就是个体关系的存储,个体存储可以忽略不计



0 0
原创粉丝点击