数据结构与算法(一) -- 基础数据结构与算法

398 阅读7分钟

一、数据结构

1.1、数据结构与算法

数据结构在我们开发过程中会听到很多次。一个好的数据结构不仅可以提高计算机的工作效率,也可以提高我们的工作效率。就好比建大楼一般, 好的大楼设计可以让工人们事半功倍,减少不必要的开支。一个好的程序并不是看它的功能有多炫酷,界面有多美观,它的中心却在于对这个程序的数据结构与算法。有数据结构的存在就会想起算法,这两者是紧密相连的。所以对数据结构的学习不仅仅是在数据结构上下功夫,算法的学习也是很有必要的。

1.2、数据结构的概念

在开发中,数据主要是由程序来操作的对象。它可以输入到计算机,被计算机来处理。例如QQ发送的文字消息就一段字符串数据。

  • 数据:程序的操作对象。
  • 数据对象:性质相同的数据元素的集。
  • 数据元素:组成数据的基本单位。
  • 数据项:一个数据元素由若干数据项组成。
//整个struct为数据结构
struct Person {
    char *name;//数据项
    char *id;//数据项
    int age;//数据项
}
int main(int argc, const char * argv[]) {
    struct Person p1;//数据元素
    struct Person p2[10];//数据对象

1.3、逻辑结构与物理结构

数据结构有两种视角,可分为两种:逻辑结构与物理结构。

1.3.1、逻辑结构

逻辑结构可分为四种:集合结构、线性结构、树形结构、图形结构。
  • 集合结构

集合结构是所有数据都属于同一个集合,数据的存放是杂乱无章的没有先后顺序可言。

  • 线性结构

    线性结构有两个特点,数据与数据之间是一对一的逻辑关系。所有符合一对一的逻辑关系都是线性结构。例如: 数组,队列,字典等。

  • 树形结构

    树形结构就是一种一对多的逻辑关系。所有一对多的逻辑关系都是树形结构。例如:二叉树等

  • 图形结构

    图形结构是一种多对多的逻辑关系。所有多对多的逻辑关系都是图形结构。

1.3.2、物理结构

所有的数据最终都会来到我们的计算机的内存中来,在计算机中的存储方式分为两种:顺序存储、链式存储。
  • 顺序存储

在内存中开辟一段连续的内存空间,将数据存放进去,所以使用顺序存储结构必须要明确的知道要开辟多大的内存空间。

优点:可以根据数据的下表地址来读取修改数据。 缺点:因为开辟的空间是固定的,所以无法对这个数据空间进行扩容。并且一旦开辟空间,无论使用空间使用与否,这片内存始终占据。

  • 链式存储

在内存中开辟一小段空间用来存储第一个数据的值与第二个数据的存储地址。第二个数据所在的地址存储着第二个数据值与第三个数据的存储地址。这样只要内存空间足够,就可以无限的开辟空间用来存储,即用多少我就开辟多少空间。

优点:一个数据紧接着一个数据,不需要提前开辟一个固定的空间,空间利用率极大的提高 缺点:查找数据必须要从头开始一个一个的往下查找,对于极度庞大的数据进行操作消耗的时间会很长。

二、算法

1.1、什么是算法

算法是解决问题的一种方法。例如高斯的高斯公式,计算面积的公式等。都是一种算法,用来解决某些问题。

1.2、数据结构与算法的关系

算法与数据结构是不可分割的,二者是缺一不可的。

1.3、算法的比较

void add1() {
    int sum = 0;
    for (int i = 0; i < 100; i++ {
        sum = sum + i;
    }
}
void add2() {
    int sum = 0;
    sum = (1 + 100) * 100 / 2;
}

这是我们在学校了很早就会认识到的高斯公式,如果按照按部就班来算,需要一个数一个数相加,这也就是需要进行100次计算才能得到结果。但是高斯公司只需要几步,极大的节约了计算时间。但是效率就绝对高吗? 这也有一个临界点,例如我只需要计算1+2, 这样套用高斯的公式就会浪费时间。但是计算的计算速度是很快的绝大部分的计算都并非1+2那么简单,所以最终的效率还是高斯公式比较实用。

1.4、算法的设计与效率

我们设计好一个算法,需要算法是否是正确的、可读性、健壮性、时间效率和存储效率。
  • 正确性:只有一个能解决问题的正确算法才是我们所需要的。所以设计好一个算法的首要目标就是这个算法能解决问题。
  • 可读性:代码是写给计算机运行的,而读代码是我们程序员来读的,一个算法如果写的晦涩难懂难以理解,这对未来的维护以及拓展将会造成很大的问题。所以并不是代码越少就是越厉害,有易懂的注释与清晰的逻辑才是最美的。
  • 健壮性:一个好的算法需要考虑到各种情况,对各种情况都要进行处理,避免某些情况造成算法的错误计算等。
  • 时间效率和存储效率:这就是我们经常听到的时间复杂度与空间复杂度,花最少的消耗与最少的消耗完成任务。

它们也是衡量一个算法的品质的高低。

1.5、时间复杂度

计算时间复杂度,我们通常用的 大O表示法, 用O()来记录算法的时间复杂度。
大O表示法有几个规则:

1. 用常数1代替运行时间中的所有常数
2. 在修改运行次数函数中,只保留最高项
3. 如果在最高阶存在且不等于1,则去除与这个项数相乘的常数


例如:
1. 常数阶
``` C
void test(int a){
    a = a + 1; //1
}
```
进行了一次运算, 所以时间复杂度记为 O(1)。

```
void test(int b){
    b = b + 1; //1
    b = b * 2; //1
    b = b * 3; //1
    b = b * 4; //1
}
```
进行了四次运算,按照第一条规则,时间复杂度仍为O(1)。

2、 线性阶
``` C
void test() {
    int sum = 0; //1
    for(int j = 0; j < n; j ++) { //n+1
        sum = sum + 1; //n
    }
}

```
进行了2n+2次运算,按照第二条,保留最高项式2n,按照第三条去掉相乘的常数,最终时间复杂度记为O(n)

3、对数阶
``` C
void test(int n){
    int count = 1;         //1
    while (count < n) {
        count = count * 2; //log2n次
    }
    
}
```
进行了 log2n+1次运算,按照第二条,保留最高项式log2n, 按照第三条去掉相乘的常数,最终时间复杂度记为O(logn)

4、平方阶
``` C
void add(int n) {
    int sum = 0;
    for(int i = 0; i < n; i++) { 
        for(int j = 0; j < n; j ++) {
            sum = sum + 1;
        }
    }
}
```
进行了n^2次计算, 最终为O(n^2)


总结:

注:后三种哪怕是运算很小的值,都会消耗极大的时间。这种不切实际的时间复杂度一般不会采纳。

1.6、空间复杂度

空间复杂度就是运行某一个算法,需要多少额外的辅助空间来进行这个算法的运行。
如果算法执行时所需要的辅助空间相对于输入数据量是一个常数,则成这个算法原地工作,辅助空间为O(1).
例如:
``` C
int temp = a;
a = b;
b = temp;
```
消耗了额外的一个变量,即空间复杂度为O(1)

``` C
int a[n] = {0};//n为常数
for(int i = 0; i < n;i++){
    a[i] = array[n-i-1];
}
for(int i = 0; i < n; i++){
    array[i] = a[i];
}
```

消耗了额外的n个空间的数组,即空间复杂度为O(n)