数据结构与算法1——基本概念

459 阅读7分钟

一、数据结构

1、基本术语

在计算机科学中,数据结构(英语:data structure)是计算机中存储、组织数据的方式。

数据:是描述客观事物的符号,是计算机中可以操作的对象,是能被计算机识别,并输入给计算机处理的符号集合。数据不仅仅包括整型、实型等数值类型,还包括字符及声音、图像、视频等非数值类型。
数据元素:是组成数据的,且有一定意义的基本单位,在计算机中通常作为整体处理。比如组成社会的人、比如动物园里的老虎。
数据项: 一个数据元素可以由若干数据项组成.比如组成人这个数据元素的耳鼻喉和四肢。
数据对象:是性质相同的数据元素的集合,是数据的子集.性质相同,是指数据元素具有相同数量和类型的数项. 类似数组中的元素保持性质一致.

2、逻辑结构与物理结构

根据视角不同,我们将数据结构分为2种: 逻辑结构与物理结构;

2.1.逻辑结构

逻辑结构: 指的是数据对象中的数据元素之间的相互关系. 逻辑结构分为四种: 集合结构,线性结构,树形结构,图形结构.

2.1.1集合结构

集合结构: 集合结构中的数据元素除了同属于一个集合外,它们之间没有其他关系. 各个数据元素是"平等"的. 它们的共同属性是:"同属于一个集合".比如动物园中的动物们.

2.1.2线性结构

线性结构中的数据元素之间是一对一的关系.常用的线性结构有:线性表,栈,队列,双队列,数组,串。

2.1.3树型结构

树形数据结构可以表示数据表素之间一对多的关系.树型结构中的数据元素之间存在一种一对多的层次关系. 常见的树形结构: 二叉树,B树,哈夫曼树,红黑树等.

2.1.4图形结构

图形结构的数据元素是多对多的关系. 常见的图形结构: 邻近矩阵,邻接表.

2.2.物理结构

物理结构,别称"存储结构". 顾名思义,指的是数据的逻辑结构在计算机的存储形式.数据元素的存储结构形式有2种: 顺序存储和链式存储。

2.2.1顺序存储

顺序存储结构,是指把数据元素存放在地址连续的存储单元里,其数据间的逻辑关系和物理关系是一致的.比如当你创建一个数组时,计算机会在内存是开辟一片连续的内存空间,然后将数据以此存储进去.顺序存储在存储时较为方便.

2.2.2链式存储

链式存储结构,是把数据元素放在任意的存储单元里,这组存储单元可以是连续的,也可以是不连续的. 数据元素的存储关系并不能反映逻辑关系,因此需要用一个指针存放数据元素的地址,这样通过地址就可以找到相关关联数据元素的位置.显然链式存储更为灵活。

二、算法

算法就是解决特定问题求解步骤的描述,在计算机中表现为指令的有限序列,并且每条指令表示一个或多个操作.

1.算法的特性

算法必须具备几个基本特性: 输入,输出,有穷性,确定性和可行性;

1.1输入输出

输入输出,很好理解. 在解决问题时必须有已知条件,当然有些算法可能没有输入. 但是算法至少有一个或多个输出.否则没有输出,没有结果.你用这个算法干吗?

1.2有穷性

有穷性: 指的是算法在执行有限的步骤之后,自动结束而不会出现无限循环,且每一个步骤在可接受的时间内完成.

1.3确定性

确定性: 算法的每一个步骤都具有确定的含义,不能出现二义性; 算法在一定条件下,只有一条执行路径,相同的输入只能有唯一的输出结果.

1.4可行性

可行性: 算法的每一步都必须是可行的,换句话说,每一步都能通过执行有限次数完成.

2.算法效率的度量方法

习惯上将算法语句重复执行的次数作为算法的时间量度

3.算法时间复杂度

3.1 时间复杂度的大O表示法

  • 用常数1取代运行时间中所有加法常数;
  • 在修改后的运行次数函数中,只保留最高阶项;
  • 如果在最高阶项存在且不是1,则去除与这个项相乘的常数;

3.2 常见的时间复杂度

3.2.1 常数阶

常数阶O(1) 无论代码执行了多少行,只要是没有循环等复杂结构,那这个代码的时间复杂度就都是O(1),如:

//1+1+1 = 3
void testSum1(int n){
    int sum = 0;                //执行1次
    sum = (1+n)*n/2;            //执行1次
    printf("testSum2:%d\n",sum);//执行1次
}

3.2.2 线性阶

void add(int x,int n){
    for (int i = 0; i < n; i++) {
        x = x+1;
    }
}

这段代码,for循环里面的代码会执行n遍,因此它消耗的时间是随着n的变化而变化的,因此这类代码都可以用O(n)来表示它的时间复杂度。

3.2.3 对数阶

int count = 1;
while(count < n){
	count = count * 2;
}

count = count * 2 ; 每次执行这句代码,就会距离n更近一步; 那么根据 x = log2^n 也就是说当循环 log2^n 次以后,这个代码就结束了。因此这个代码的时间复杂度为:O(logn)

3.2.4 平方阶

//x=x+1; 执行n*n次
void add3(int x,int n){
    for (int i = 0; i< n; i++) {
        for (int j = 0; j < n ; j++) {
            x=x+1;
        }
    }
}

这段代码为双重for循环,循环此时为n*n, 循环次数为O(n^2).

3.2.5 立方阶

即三重循环,O(n^3).

3.2.6 nlogn阶

O(nlog n).

3.2.7 指数阶

O(2^n).
常见时间复杂度比较
O(1) < O(log n) < O(n) < O(nlog n) < O(n^2) < O(n^3) < O(2^n) < O(n!) < O(n^n)

3.3 最坏情况与最好情况

最坏的情况运行时间是一种保证, 那就是运行时间将不会比这更坏了. 在应用中,这是一种最重要的需求,通常除非特别指定,我们提到的运行时间都是最坏情况下的运行时间.

3.4 算法空间复杂度

算法的空间复杂度通过计算算法所需的存储空间实现,算法空间复杂度的计算公式 记做: S(n) = n(f(n)),其中,n为问题的规模,f(n)为语句句关于n所占存储空间的函数
一般情况下, 一个程序在机器上执行时,除了需要寄存本身所用的指令,常数,变量和输入数据外,还需要一些对数据进行操作的辅助存储空间. 其中,对于输入数据所占的具体存储量取决于问题本身,与算法无关. 这样只需要分析该算法在实现时所需要的辅助空间就可以了. 如果算法执行时所需要的辅助空间相对于输入数据量是一个常数,则成这个算法原地工作,辅助空间为O(1).

问题: 数组逆序,将一维数组a中的n个数逆序存放在原数组中.
    int n = 5;
    int a[10] = {1,2,3,4,5,6,7,8,9,10};
    
    //算法实现(1)
    /*
    算法(1),仅仅通过借助一个临时变量temp,与问题规模n大小无关,所以其空间复杂度为O(1);
    */
    int temp;
    for(int i = 0; i < n/2 ; i++){
        temp = a[i];
        a[i] = a[n-i-1];
        a[n-i-1] = temp;
    }
    for(int i = 0;i < 10;i++)
    {
        printf("%d\n",a[i]);

    }
    
    //算法实现(2)
    /*
     算法(2),借助一个大小为n的辅助数组b,所以其空间复杂度为O(n).
    */
    int b[10] = {0};
    for(int i = 0; i < n;i++){
        b[i] = a[n-i-1];
    }
    for(int i = 0; i < n; i++){
        a[i] = b[i];
    }
    for(int i = 0;i < 10;i++)
    {
        printf("%d\n",a[i]);
    }
    

算法(1),仅仅通过借助一个临时变量temp,与问题规模n大小无关,所以其空间复杂度为O(1);
算法(2),借助一个大小为n的辅助数组b,所以其空间复杂度为O(n).