基础夯实:基础数据结构与算法

Posted 一只努力学习的程序猿

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了基础夯实:基础数据结构与算法相关的知识,希望对你有一定的参考价值。

上一章我们说了常见的10种数据结构,接下来我们说常见的10种算法。

上一章地址:基础夯实:基础数据结构与算法(一),不怎么清楚的可以去瞅瞅。

常见的10种算法

数据结构研究的内容:就是如何按一定的逻辑结构,把数据组织起来,并选择适当的存储表示方法把逻辑结构组织好的数据存储到计算机的存储器里。

算法研究的目的是为了更有效的处理数据,提高数据运算效率。数据的运算是定义在数据的逻辑结构上,但运算的具体实现要在存储结构上进行。

一般有以下几种常用运算:

  • 检索:检索就是在数据结构里查找满足一定条件的节点。一般是给定一个某字段的值,找具有该字段值的节点。
  • 插入:往数据结构中增加新的节点。
  • 删除:把指定的结点从数据结构中去掉。
  • 更新:改变指定节点的一个或多个字段的值。
  • 排序:把节点按某种指定的顺序重新排列。例如递增或递减。

1、递归算法

 递归算法:是一种直接或者间接地调用自身的算法。在计算机编写程序中,递归算法对解决一大类问题是十分有效的,它往往使算法的描述简洁而且易于理解。

递归过程一般通过函数或子过程来实现。
递归算法的实质:是把问题转化为规模缩小了的同类问题的子问题。然后递归调用函数(或过程)来表示问题的解。
递归算法解决问题的特点
  1.   递归就是在过程或函数里调用自身。
  2.   在使用递归策略时,必须有一个明确的递归结束条件,称为递归出口。
  3.   递归算法解题通常显得很简洁,但递归算法解题的运行效率较低。所以一般不提倡用递归算法设计程序。
  4.   在递归调用的过程当中系统为每一层的返回点、局部量等开辟了栈来存储。递归次数过多容易造成栈溢出等。所以一般不提倡用递归算法设计程序。

下面来详细分析递归的工作原理。

先看看C语言中函数的执行方式,需要了解一些关于C程序在内存中的组织方式:

堆的增长方向为从低地址到高地址向上增长,而栈的增长方向刚好相反(实际情况与CPU的体系结构有关)。

当C程序中调用了一个函数时,栈中会分配一块空间来保存与这个调用相关的信息,每一个调用都被当作是活跃的。

 

栈上的那块存储空间称为活跃记录或者栈帧。

栈帧由5个区域组成:输入参数、返回值空间、计算表达式时用到的临时存储空间、函数调用时保存的状态信息以及输出参数,参见下图:

 

 

栈是用来存储函数调用信息的绝好方案,然而栈也有一些缺点:

栈维护了每个函数调用的信息直到函数返回后才释放,这需要占用相当大的空间,尤其是在程序中使用了许多的递归调用的情况下。

除此之外,因为有大量的信息需要保存和恢复,因此生成和销毁活跃记录需要消耗一定的时间。

我们需要考虑采用迭代的方案。幸运的是我们可以采用一种称为尾递归的特殊递归方式来避免前面提到的这些缺点。

 

例题1:计算n!

计算n的阶乘,数学上的计算公式为:

n!=n×(n-1)×(n-2)……2×1

使用递归的方式,可以定义为:

 

以递归方式实现阶乘函数的实现:

#define _CRT_SECURE_NO_WARNINGS  //避免scanf报错
#include <stdio.h>

int main(void)

    int sumInt = fact(3);
    printf("3的阶乘为:%d\\n", sumInt);
    system("PAUSE");//结束不退出


//递归求阶乘
int fact(int n) 
    if (n < 0)
        return 0;
    else if (n == 0 || n == 1)
        return 1;
    else
        return n * fact(n - 1);

例题2:斐波那契数列 

#define _CRT_SECURE_NO_WARNINGS  //避免scanf报错
#include <stdio.h>

void main(void)

    printf("%d \\n", fibonacci(10));
    system("PAUSE");//结束不退出


//斐波那契数列,第一二项为1;后面的每一项为前两项之和
int fibonacci(int a)
    if (a == 1 || a == 2)
    
        return 1;
    
    else
        return fibonacci(a - 1) + fibonacci(a - 2);
    

例题3:递归将整形数字转换为字符串

#define _CRT_SECURE_NO_WARNINGS  //避免scanf报错
#include <stdio.h>

void main(void)

    char str[100];
    int i;
    printf("enter a integer:\\n");
    scanf("%d", &i);
    toString(i, str);
    puts(str);
    system("PAUSE");//结束不退出


//递归将整形数字转换为字符串 
int toString(int i, char str[])
    int j = 0;
    char c = i % 10 + \'0\';
    if (i /= 10)
    
        j = toString(i, str) + 1;
    
    str[j] = c;
    str[j + 1] = \'\\0\';
    return j;

例题4:汉诺塔

#define _CRT_SECURE_NO_WARNINGS  //避免scanf报错
#include <stdio.h>

//递归汉诺塔
void hanoi(int i, char x, char y, char z)
    if (i == 1)
        printf("%c -> %c\\n", x, z);
    
    else
        hanoi(i - 1, x, z, y);
        printf("%c -> %c\\n", x, z);
        hanoi(i - 1, y, x, z);
    


void main(void)

    hanoi(10, \'A\', \'B\', \'C\');
    system("PAUSE");//结束不退出

例题5:猴子吃桃

#define _CRT_SECURE_NO_WARNINGS  //避免scanf报错
#include <stdio.h>

//猴子吃桃,每天吃一半再多吃一个,第十天想吃时候只剩一个 
int chitao(int i)
    if (i == 10)
        return 1;
    
    else
        return (chitao(i + 1) + 1) * 2;
    


void main(void)

    printf("%d", chitao(5));
    system("PAUSE");//结束不退出

 例题6:N皇后问题

#define _CRT_SECURE_NO_WARNINGS  //避免scanf报错
#include <stdio.h>

/*======================N皇后问题========================*/
#define N 100

int q[N];//列坐标

//输出结果
void dispasolution(int n)

    static int count = 0;
    printf("  第%d个解:", ++count);
    for (int i = 1; i <= n; i++)
    
        printf("(%d,%d) ", i, q[i]);
    
    printf("\\n");


//判断位置(i,j)能否放皇后
int place(int i, int j)

    //第一个皇后总是可以放
    if (i == 1)
        return 1;
    //其他皇后不能同行,不能同列,不能同对角线
    int k = 1;

    //k~i-1是已经放置了皇后的行
    while (k < i)
    
        if ((q[k] == j) || (abs(q[k] - j) == abs(i - k)))
            return 0;
        k++;
    
    return 1;


//放置皇后
void queen(int i, int n)

    if (i > n)dispasolution(n);
    else
    
        for (int j = 1; j <= n; j++)
        
            if (place(i, j)==1)
            
                q[i] = j;
                queen(i + 1, n);
            
        
    



int main()

    queen(1, 4);
    system("PAUSE");//结束不退出

 

2、排序算法

排序是程序设计中常做的操作,初学者往往只知道冒泡排序算法,其实还有很多效率更高的排序算法,比如希尔排序、快速排序、基数排序、归并排序等。

不同的排序算法,适用于不同的场景,本章最后从时间性能,算法稳定性等方面,分析各种排序算法。

排序算法,还分为内部排序算法和外部排序算法,之间的区别是,前者在内存中完成排序,而后者则需要借助外部存储器。

这里介绍的是内部排序算法。

冒泡排序:

起泡排序,别名“冒泡排序”,该算法的核心思想是将无序表中的所有记录,通过两两比较关键字,得出升序序列或者降序序列。

例如,对无序表49,38,65,97,76,13,27,49进行升序排序的具体实现过程如图 1 所示:

 

 如下冒泡排序例子

#define _CRT_SECURE_NO_WARNINGS  //避免scanf报错
#include <stdio.h>

//冒泡排序
//交换 a 和 b 的位置的函数
void swap(int *a, int *b);
void main()

    int array[8] =  49, 38, 65, 97, 76, 13, 27, 49 ;
    int i, j;
    int key;
    //有多少记录,就需要多少次冒泡,当比较过程,所有记录都按照升序排列时,排序结束
    for (i = 0; i < 8; i++)
        key = 0;//每次开始冒泡前,初始化 key 值为 0
        //每次起泡从下标为 0 开始,到 8-i 结束
        for (j = 0; j + 1<8 - i; j++)
            if (array[j] > array[j + 1])
                key = 1;
                swap(&array[j], &array[j + 1]);
            
        
        //如果 key 值为 0,表明表中记录排序完成
        if (key == 0) 
            break;
        
    
    for (i = 0; i < 8; i++)
        printf("%d ", array[i]);
    
    system("PAUSE");//结束不退出

void swap(int *a, int *b)
    int temp;
    temp = *a;
    *a = *b;
    *b = temp;

快速排序:

快速排序算法是在起泡排序的基础上进行改进的一种算法,

其实现的基本思想是:通过一次排序将整个无序表分成相互独立的两部分,其中一部分中的数据都比另一部分中包含的数据的值小,然后继续沿用此方法分别对两部分进行同样的操作,

直到每一个小部分不可再分,所得到的整个序列就成为了有序序列。

该操作过程的具体实现代码为:

#define _CRT_SECURE_NO_WARNINGS  //避免scanf报错
#include <stdio.h>
#include <stdlib.h>
#define MAX 9
//单个记录的结构体
typedef struct 
    int key;
SqNote;
//记录表的结构体
typedef struct 
    SqNote r[MAX];
    int length;
SqList;
//此方法中,存储记录的数组中,下标为 0 的位置时空着的,不放任何记录,记录从下标为 1 处开始依次存放
int Partition(SqList *L, int low, int high)
    L->r[0] = L->r[low];
    int pivotkey = L->r[low].key;
    //直到两指针相遇,程序结束
    while (low<high) 
        //high指针左移,直至遇到比pivotkey值小的记录,指针停止移动
        while (low<high && L->r[high].key >= pivotkey) 
            high--;
        
        //直接将high指向的小于支点的记录移动到low指针的位置。
        L->r[low] = L->r[high];
        //low 指针右移,直至遇到比pivotkey值大的记录,指针停止移动
        while (low<high && L->r[low].key <= pivotkey) 
            low++;
        
        //直接将low指向的大于支点的记录移动到high指针的位置
        L->r[high] = L->r[low];
    
    //将支点添加到准确的位置
    L->r[low] = L->r[0];
    return low;

void QSort(SqList *L, int low, int high)
    if (low<high) 
        //找到支点的位置
        int pivotloc = Partition(L, low, high);
        //对支点左侧的子表进行排序
        QSort(L, low, pivotloc - 1);
        //对支点右侧的子表进行排序
        QSort(L, pivotloc + 1, high);
    

void QuickSort(SqList *L)
    QSort(L, 1, L->length);

void main() 
    SqList * L = (SqList*)malloc(sizeof(SqList));
    L->length = 8;
    L->r[1].key = 49;
    L->r[2].key = 38;
    L->r[3].key = 65;
    L->r[4].key = 97;
    L->r[5].key = 76;
    L->r[6].key = 13;
    L->r[7].key = 27;
    L->r[8].key = 49;
    QuickSort(L);
    for (int i = 1; i <= L->length; i++) 
        printf("%d ", L->r[i].key);
    
    system("PAUSE");//结束不退出

更多点击  排序算法:http://data.biancheng.net/sort/ (插入排序算法、快速排序算法、选择排序算法、归并排序和基数排序等)

3、二分查找算法

 二分査找就是折半查找,

其基本思想是:首先选取表中间位置的记录,将其关键字与给定关键字 key 进行比较,若相等,则査找成功;

若 key 值比该关键字值大,则要找的元素一定在右子表中,则继续对右子表进行折半查找;

若 key 值比该关键宇值小,则要找的元素一定在左子表中,继续对左子表进行折半査找。

如此递推,直到査找成功或査找失败(或査找范围为 0)。

例如:

要求用户输入数组长度,也就是有序表的数据长度,并输入数组元素和査找的关键字。

程序输出查找成功与否,以及成功时关键字在数组中的位置。

例如,在有序表 11、13、18、 28、39、56、69、89、98、122 中査找关键字为 89 的元素。

#define _CRT_SECURE_NO_WARNINGS  //避免scanf报错
#include <stdio.h>
int binary_search(int key, int a[], int n) //自定义函数binary_search()

    int low, high, mid, count = 0, count1 = 0;
    low = 0;
    high = n - 1;
    while (low<high)    //査找范围不为0时执行循环体语句
    
        count++;    //count记录査找次数
        mid = (low + high) / 2;    //求中间位置
        if (key<a[mid])    //key小于中间值时
            high = mid - 1;    //确定左子表范围
        else if (key>a[mid])    //key 大于中间值时
            low = mid + 1;    //确定右子表范围
        else if (key == a[mid])    //当key等于中间值时,证明查找成功
        
            printf("查找成功!\\n 查找 %d 次!a[%d]=%d", count, mid, key);    //输出査找次数及所査找元素在数组中的位置
            count1++;    //count1记录查找成功次数
            break;
        
    
    if (count1 == 0)    //判断是否查找失敗
        printf("查找失敗!");    //査找失敗输出no found
    return 0;

int main()

    int i, key, a[100], n;
    printf("请输入数组的长度:\\n");
    scanf("%d", &n);    //输入数组元素个数
    printf("请输入数组元素:\\n");
    for (i = 0; i<n; i++)
        scanf("%d", &a[i]);    //输入有序数列到数组a中
    printf("请输入你想查找的元素:\\n");
    scanf("%d", &key);    //输入要^找的关键字
    binary_search(key, a, n);    //调用自定义函数
    printf("\\n");
    system("PAUSE");//结束不退出;

 

4、搜索算法

搜索算法是利用计算机的高性能来有目的的穷举一个问题解空间的部分或所有的可能情况,从而求出问题的解的一种方法。

现阶段一般有枚举算法、深度优先搜索广度优先搜索A*算法回溯算法、蒙特卡洛树搜索、散列函数等算法。

在大规模实验环境中,通常通过在搜索前,根据条件降低搜索规模;

根据问题的约束条件进行剪枝;利用搜索过程中的中间解,避免重复计算这几种方法进行优化。

这里介绍的是深度优先搜索,感兴趣的可以百度查询更多搜索算法。

内容很多,大家可以百度查询感兴趣的用法:也可以点击 深度优先搜索 查看更多。

深度优先搜索

  • 深度优先遍历首先访问出发点v,并将其标记为已访问过;然后依次从v出发搜索v的每个邻接点w。若w未曾访问过,则以w为新的出发点继续进行深度优先遍历,直至图中所有和源点v有路径相通的顶点均已被访问为止。
  • 若此时图中仍有未访问的顶点,则另选一个尚未访问的顶点作为新的源点重复上述过程,直至图中所有顶点均已被访问为止。

深度搜索与广度搜索的相近,最终都要扩展一个结点的所有子结点.

  区别在于对扩展结点过程,深度搜索扩展的是E-结点的邻接结点中的一个,并将其作为新的E-结点继续扩展,当前E-结点仍为活结点,待搜索完其子结点后,回溯到该结点扩展它的其它未搜索的邻接结点。

而广度搜索,则是扩展E-结点的所有邻接结点,E-结点就成为一个死结点。

5、哈希算法

1. 什么是哈希

Hash,一般翻译做散列、杂凑,或音译为哈希,是一个典型的利用空间换取时间的算法,把任意长度的输入(又叫做预映射pre-image)通过散列算法变换成固定长度的输出,该输出就是散列值。

如有一个学生信息表:

学生的学号为:年纪+学院号+班级号+顺序排序号【如:19(年纪)+002(2号学院)+01(一班)+17(17号)---à190020117】类似于一个这样的信息,

当我们需要找到这个学号为【190020117】的学生,在不适用哈希的时候,我们通常是使用一个顺序遍历的方式在数据中进行查询大类,再查询子类得到,

这样的作法很明显不够快 ,需要O(n)左右的时间花费,对于大型的数据规模而言这显然不行,

而哈希的做法是,根据一定的规律(比如说年纪不存在过老和过小的情况,以此将【190020117】进行压缩成一个简短的数据如:

【192117】)并且将这个数据直接作用于内存的地址,届时我们查询【190020117】只需要进行一次压缩并访问【192117】这个地址即可,而这个压缩的方法(函数),就可以称之为哈希函数。

一般的对于哈希函数需要考虑如下内容:

  1. 计算散列地址所需要的时间(即hash函数本身不要太复杂)
  2. 关键字的长度
  3. 表长(不宜过长或过短,避免内存浪费和算力消耗)
  4. 关键字分布是否均匀,是否有规律可循
  5. 设计的hash函数在满足以上条件的情况下尽量减少冲突

 

2.哈希与哈希表

在理解了哈希的思维之后,我们要了解什么是哈希表,哈希表顾名思义就是经过哈希函数进行转换后的一张表,

通过访问哈希表,我们可以快速查询哈希表,从而得出所需要得到的数据,构建哈希表的核心就是要考虑哈希函数的冲突处理(即经过数据压缩之后可能存在多数据同一个地址,需要利用算法将冲突的数据分别存储)。

冲突处理的方法有很多,最简单的有+1法,即地址数直接+1,当两个数据都需要存储进【2019】时,可以考虑将其中的一个存进【2020】

此外还有,开放定址法,链式地址发,公共溢出法,再散列法,质数法等等,各方法面对不同的数据特征有不同的效果。

 

3.哈希的思维

Hash算法是一个广义的算法,也可以认为是一种思想,使用Hash算法可以提高存储空间的利用率,可以提高数据的查询效率,也可以做数字签名来保障数据传递的安全性。

所以Hash算法被广泛地应用在互联网应用中。

比如,利用哈希的思维在O(1)的复杂度情况下任意查询1000以内所有的质数(在创建是否是质数的时候并不是O(1)的复杂度),

注意本样例只是演示思维,面对本需求可以有更好的空间利用方式(本写法比较浪费空间,仅供了解)。

 如下例子:

电话聊天狂人】

给定大量手机用户通话记录,找出其中通话次数最多的聊天狂人。

输入格式:
输入首先给出正整数N(≤10​5​​),为通话记录条数。随后N行,每行给出一条通话记录。简单起见,这里只列出拨出方和接收方的11位数字构成的手机号码,其中以空格分隔。

输出格式:
在一行中给出聊天狂人的手机号码及其通话次数,其间以空格分隔。如果这样的人不唯一,则输出狂人中最小的号码及其通话次数,并且附加给出并列狂人的人数。
输入样例:

4
13005711862 13588625832
13505711862 13088625832
13588625832 18087925832
15005713862 13588625832

输出样例:

13588625832 3

#define _CRT_SECURE_NO_WARNINGS  //避免scanf报错
#include <stdio.h>
#include <string.h>
#include <math.h>
#include <stdlib.h>
#include <ctype.h>
#define MAX 400000  /** 定义 最大 数组 大小 **///(感觉没啥用 但是最好尽可能开大点,但是不要太大,不要超出系统可建造范围)
typedef struct Node *Hash; /**新的路程又开始了 这次准备用数组来做哈希 还有双向平方处理冲突**/
struct Node
    char phone[15];
    int num;
;

int maxInt(int x, int y)

    if (x>y) return x;
    else return y;


char* minstr(char *x, char *y)

    if (strcmp(x, y)>0) return y;
    else return x;

int nextprime(const int n)

    int p = (n % 2 == 1) ? n + 2 : n + 1; /**先找一个大于N的奇数**/
    int i;
    while (p<MAX)
    
        for (i = (int)sqrt(p); i >= 2; i--)   /**然后再判断是不是素数**/
        if (p%i == 0) break;
        if (i<2) return p;        /**是 那就返回这个数**/
        else p += 2;/**不是 那就下一个奇数**/
    

int deal(char *s, int p) /**然后把字符串映射成下标 (映射的方式很多很多,随便猜一个靠谱的就行了)**/

    int index = (atoi(s + 2)) % p;
    return index;

int insert(Hash h, int pos, char *s, int p, int Max) /**哈希查找的插入实现 ,分别是哈希数组,数组位置,身份证号,数组最大大小, MAX 看到代码最后就明白了**/

    int i, posb = pos;             /**备份pos值方便双向平方查找**/
    for (i = 1;; i++)
    
        if (strcmp(h[pos].phone, "") == 0) /**如果为pos的值空直接插入**/
        
            strcpy(h[pos].phone, s);
            h[pos].num++;
            Max = max(Max, h[pos].num);
            break;
        
        else
        
            if (strcmp(h[pos].phone, s) == 0) /**不为空的话,就看看身份证号是不是想等**/
            
                h[pos].num++;
                Max = maxInt(Max, h[pos].num);
                break;
            
            else
             //原p%2==1
 

以上是关于基础夯实:基础数据结构与算法的主要内容,如果未能解决你的问题,请参考以下文章

游戏陪玩系统开发技术的碰撞与融合,夯实基础很重要

数据库系统学习与基础夯实——基础知识

夯实基础,编译器原理前端部分浅析

JavaScript夯实基础系列:原型

夯实基础系列:文本识别算法:RARE(Robust Scene Text Recognition with Automatic Rectification)核心代码

夯实基础,不能忽视的“数据库”