数据结构与算法 01、 基本认知 & 线性表初探

本文用来了解数据结构与算法的一些基础。

书籍推荐:《大话数据结构 - 程杰 著》

一、算法

1、算法特性

算法:解决特定问题的一步步思路方法步骤。在计算机中其表现在指令的有限序列,每个指令包含一或多个操作。

算法特性

  • 有输入输出
  • 有穷性 - 有限步骤内完成,不能无限循环
  • 确定性 - 有确定结果,不能存异
  • 可行性 - 每一行代码必须示可执行的  

算法的设计要求

  • 正确性 - 
  • 可读性 - 
  • 健壮性 - 
  • 时间效率高&存储量低

2、算法的复杂度

O表示法

  • 用常数1取代运行时间中所有常数 3 --> O(1)
  • 运行次数函数中,只保留最高阶 n3+2n2+3 --> O(n3)
  • 最高阶存在且不等于1时,去除与这个高阶项 相乘的常数 3n3+2n2+n+4 --> O(n3)

1、时间复杂度

时间复杂度术语:

  • 常数阶 - 3
  • 对数阶 - logan  --> 以 a 为底 n 的对数 ax=n, x=logan 
  • 线性阶 - 2n+1
  • 平方阶 - n2
  • 立方阶 - n3
  • nlog 阶 - n*logn 即 n * log2n. 一般以2为底习惯性省略2的书写.  
  • 指数阶 - O(2n) 或者 O(n!) --> 一般不考虑!除非是 n 非常的小,否则会造成可怕的噩梦般的时间消耗。这是一种不切实际的算法时间复杂度,一般不会考虑。

1. 示例

时间复杂度大小关系:O(1) < O(logn) < O(n) < O(nlogn) < O(n2) < O(n3) < O(2n) < O(n!) < O(nn)

2. 代码示例

常数阶:

// 常数阶时间复杂度计算 O(1) <-- 常数用 1 代替
// 1+1+1 = 3 --> O(1)
void testSum1(int n){
    int sum = 0;                // 执行1次
    sum = (1+n)*n/2;            // 执行1次
    printf("testSum1:%d
",sum);// 执行1次
}

// 1+1+1+1+1+1+1 = 7 --> O(1)
void testSum2(int n){
    int sum = 0;                // 执行1次
    sum = (1+n)*n/2;            // 执行1次
    sum = (1+n)*n/2;            // 执行1次
    sum = (1+n)*n/2;            // 执行1次
    sum = (1+n)*n/2;            // 执行1次
    sum = (1+n)*n/2;            // 执行1次
    printf("testSum2:%d
",sum);// 执行1次
}
// x=x+1; --> 执行1次
void add(int x){
    x = x+1;
}

对数阶:

// 对数阶 -- 不论是 以几为底n的对数,都会归到 logn 表示
// 2的x次方等于n x = log2n --> O(logn)
void testA(int n){
    int count = 1;         //执行1次
    // n = 10
    while (count < n) {
        count = count * 2;
    }
}
// 5的x次方等于n x = log5n --> O(logn) void testF(int n){ int count = 1; //执行1次 // n = 100 while (count < n) { count = count * 5; } }

线性阶:

// 线性阶时间复杂度 
// x=x+1; 执行n次 --> O(n)
void add2(int x,int n){
    for (int i = 0; i < n; i++) {
        x = x+1;
    }
}

// 1+(n+1)+n+1 = 3+2n --> O(n)
void testSum3(int n){
    int i,sum = 0;               // 执行1次
    for (i = 1; i <= n; i++) {   // 执行n+1次
        sum += i;                // 执行n次
    }
    printf("testSum3:%d
",sum); // 执行1次
}

平方阶:

// 平方阶
// x=x+1; 执行 n*n 次 --> O(n^2)
void add3(int x,int n){
    for (int i = 0; i< n; i++) {
        for (int j = 0; j < n ; j++) {
            x=x+1;
        }
    }
}

// n+(n-1)+(n-2)+...+1 = n(n-1)/2 = n^2 / 2 + n/2 --> O(n^2)
// 等差数列公式:sn = n(a1+an)/2 <-- (1+100)*100/2 = 5050
void testSum4(int n){
    int sum = 0;
    for(int i = 0; i < n;i++)
        for (int j = i; j < n; j++) {
            sum += j;
        }
    printf("textSum4:%d",sum);
}

// 1+(n+1)+n(n+1)+n^2+n^2 = 2+3n^2+2n --> O(n^2)
void testSum5(int n){
    int i,j,x=0,sum = 0;           // 执行1次
    for (i = 1; i <= n; i++) {     // 执行n+1次
        for (j = 1; j <= n; j++) { // 执行n(n+1)
            x++;                   // 执行n*n次
            sum = sum + x;         // 执行n*n次
        }
    }
    printf("testSum5:%d
",sum);
}

立方阶:

// 立方阶
void testB(int n){
    int sum = 1;                         // 执行1次
    for (int i = 0; i < n; i++) {        // 执行n次
        for (int j = 0 ; j < n; j++) {   // 执行n*n次
            for (int k = 0; k < n; k++) {// 执行n*n*n次
                sum = sum * 2;           // 执行n*n*n次
            }
        }
    }
}

3. 时间复杂度的大小

2、空间复杂度

空间复杂度:计算算法过程中,中间变量所需的存储空间。

公式 S(n) = n(f(n)) --> n:问题的规模; f(n):关于 n 所占存储空间的函数

程序空间计算考虑因素:

  • 寄存本身的指令
  • 常数
  • 变量
  • 输入
  • 对数据进行操作的辅助空间

在考量算法的空间复杂度时,主要考虑的是算法执行时所需要的辅助空间

代码示例:

// 数组逆序,将一维数组 a 中的 n 个数逆序存放在原数组中.
int main(int argc, const char * argv[]) {
    // insert code here...
    printf("Hello, World!
");
   
    int n = 5;
    int a[10] = {1,2,3,4,5,6,7,8,9,10};
    
    // 算法实现1 --> O(1)
    int temp;// 1中间变量
    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
",a[i]);
    }
    
    // 算法实现2 --> O(n)
    int b[10] = {0};// n
    for(int i = 0; i < n;i++) {
        b[i] = a[n-i-1];// n个中间值
    }
    for(int i = 0; i < n; i++) {
        a[i] = b[i];
    }
    for(int i = 0;i < 10;i++) {
        printf("%d
",a[i]);
    }
    
    return 0;
}

二、数据结构 概念

文章 数据结构与算法 0 中对数据结构的一些基本概念进行了简单了解,这里进行更详细的介绍。

1、基本数据单位

 

2、逻辑结构与物理结构

1)逻辑结构

逻辑结构,描述的是数据元素之间的相互关系。它只是代表数据间所存在的一个逻辑关系,例如:人与人、树干与树枝等。

1. 集合结构

集合结构:同属于一类但并无什么特定关系。例:动物便是一个集合,包含人、猫猫、狗狗...;植物也是一个集合,花草树木...。 

2. 线性结构

线性结构:数据与数据间符合一对一的关系。

常见线性结构线性表链表数组字符串(多个字符组合而成);

       队列 因其特殊的读取方式,属于特殊的线性结构。

3. 树结构

树结构特点:数据关系之间是一对多的关系。

4. 图形结构

图形结构特点:数据与数据间多对多的关系。 

2)物理结构(也称存储结构)

物理结构:指数据的逻辑结构计算机中的存储形式

数据是数据元素的集合,那么物理结构实际上就是:如何把数据元素存储到计算机的存储器中。存储器是针对内存而言的。(硬盘、软盘、光盘等外部存储器的数据组织通常用文件结构来描述。)

对于所有数据,最终它们都是需要存储到计算机内存中去的,数据元素在内存中的存储方式(存储结构形式)有 2 种:顺序存储 链式存储

1. 顺序存储结构

开辟一段连续的内存空间,按顺序依次存储进去,其数据间的逻辑关系和物理关系是一致的。简单来说就是:排队占位。按序排好队,每人占一小段空间。例:数组 arr[10].

优点:查询快;

缺点:增删麻烦,每次都需要对数据进行重新的排序。

2. 链式存储结构

链式存储结构:把数据元素存放在任意的存储单元里,这组存储单元可以是连续的也可不连续,即:不需要提前开辟连续的内存空间。

这种数据元素的存储关系并不能反映其逻辑关系,因此需要用一个指针存放数据元素的地址,这样就可以通过地址找到相关元素的位置。

 

优点:增删便捷,增加找到位置放进去即可,删除后删除的元素需要释放为nil防止野指针;

缺点:查询很慢。

3、抽象数据类型

1. 数据类型

数据类型:是指一组性质相同的值的集合 以及 定义子在此集合上的一些操作的总称。

数据类型是按照值的不同进行划分的。在 C 语言中,按照值的不同,数据类型可分为 2 类:

  • 原子类型:不可再分解的基本类型,包括:整型、实型、字符型等;
  • 结构类型:由若干个类型组合而成,可以再分解。例如:整形数组,它是由若干整型数据组成的。

例如 int a ,b; 这意味着 在给变量 a、b 赋值时不能超出 int 的取值范围,且变量之间的运算只能是 int 类型所允许的运算。

因不同计算机不同的硬件系统,编程语言最终都会被编译成底层语言。然而对于高级语言来讲,目的知识实现 a+b 的运算,不关心计算机内部或 CPU 都具体做了什么,这里其实就引出了 抽象 的概念。

抽象:是指抽取出事物具有的普遍性的本质。它抽出问题的特征而忽略非本质的细节,相当于是对一个具体事物的概括。即:隐藏了繁杂的细节,保留实现目标所必须的信息。例如:一本书籍的章节目录。

2. 抽象数据类型

抽象数据类型(Abstract Data Type, ADT):指 一个数学模型 及其 定义在该模型上的一组操作。抽象数据类型的定义仅仅取决于它的一组逻辑特性,与计算及内部如何表示和实现无关。

比如上述的 整型 int 的例子,各个计算机,不论电脑、pad、手机等等,都拥有“整型”类型,也需要证书间的运算,so 整型 其实就是一个抽象数据类型

除了已定义并实现的,抽象数据类型还包括编程者自己定义的数据类型,如坐标,定义一个 point 的抽象数据类型,它有 x、y、z 三个整型变量,我们就可以通过 point 数据变量来知道这一点的坐标。

抽象数据类型体现了程序设计问题分解抽象信息隐藏的特性

描述抽象数据类型的标准格式

/**描述抽象数据类型的标准格式
    ADT  抽象数据类型名
    Data
        数据元素之间的逻辑关系定义
    Operation
        操作1
            初始条件
            操作结果描述
        操作2
            ... ...
        操作 n
            ... ...
    endADT
 */

三、线性表的存储方式

线性表(List):零个或多个数据元素有序序列。有序 有限.

如下图,将线性表用数学语言进行定义,记为:(a1, ..., ai-1, ai, ai+1, ..., an),则表中 ai-1 领先于 ai, ai 领先于 ai+1,称 ai-1 是 ai 的直接前驱元素ai+1 是 ai 的直接后继元素。当 i=1,2,...,n-1 时,ai 有且仅有一个直接后继,当 i=2,3,...,n 时,ai 有且仅有一个直接前驱。

由上可知,线性表元素的个数 n(n≥0) 定义为线性表的长度,当 n=0 时,称为空表。 

对于非空的线性表线性结构,特点如下:

  • 存在唯一的一个被称作“第一个”的数据元素;
  • 存在唯一的一个被称作“最后一个”的数据元素;
  • 除了第一个之外,结构中的每个元素都有一个前驱
  • 除了最后一个外,结构中的每个元素都有一个后继

线性表的抽象数据类型:

/**线性表的抽象数据类型定义
   ADT  线性表 (List)
   Data
       线性表的数据对象集合为{a1, a2,  ..., an},每个元素的类型均为 DataType。其中,除第一个元素 a1 外,每个元素都有且仅有一个直接前驱元素,除最后一个元素 an 外,每个元素都有且仅有一个直接后继元素。数据元素之间的关系是一对一。
   Operation
       InitList (*L) :初始化操作,建立一个空的线性表 L
       ListEmpty (L) :若线性表为空,返回 true,否则返回 false
       ClearList (L) :将线性表清空
       GetElem(L, i, *e) : 将线性表 L 的第 i 个位置元素值返回给 e
       LocateElem(L, e) : 在线性表 L 中查找与给定值 e 相等的元素,成功返回该元素在表中序号;否则返回 0 表示查找失败
       ListInsert(*L, i, e) : 在线性表 L 中 第 i 个位置插入新元素 e
       ListDelete(*L, i, *e) : 删除线性表 L 中 第 i 个位置元素,并用 e 返回其值
       ListLength(L) :返回 线性表 L 的元素个数
   endADT
*/

1、线性表的顺序存储

线性表的顺序存储结构,指 用一段地址连续的存储单元依次存储线性表的数据元素

1. 代码示例

顺序存储的线性表,创建、初始化和增删:

  1 /// 顺序存储的线性表
  2 
  3 #define MAXSize 100
  4 
  5 typedef struct {
  6     int *data;
  7     int length;
  8 }MyList;
  9 
 10 MyList gList;
 11 
 12 // 初始化一个线性表
 13 int initList(MyList *L) {
 14     
 15     L->data = malloc(sizeof(MyList)*MAXSize);
 16     if (!L->data) {// 若分配空间失败,退出
 17         exit(0);
 18     }
 19     L->length = 0;// 空表长度0
 20     return 1;
 21 }
 22 
 23 // 线性表的插入
 24 /**
 25  在 L 中 的 i 位置 添加插入 ele
 26  插入位置的后面的元素都要后移一位
 27  */
 28 int insertList(MyList *L, int i, int ele) {
 29     
 30     if (i < 0 || i > L->length + 1) {// 位置合法
 31         exit(0);
 32     }
 33     if (L->length == MAXSize) {// 最大空间已满
 34         return 0;
 35     }
 36     
 37     if (i <= L->length) {
 38         for (int j = L->length-1; j >= i; j--) {
 39             // 添加的元素 i 位置后的元素都后移一位
 40             L->data[j+1] = L->data[j];
 41         }
 42     }
 43     L->data[i] = ele;
 44     ++L->length;// 表长度+1
 45     return 1;
 46 }
 47 
 48 // 线性表的取值
 49 // 取某个值
 50 int getElemFrmList(MyList L, int i, int *elem) {
 51     
 52     if (i < 0 || i > L.length + 1) {// 位置合法
 53         exit(0);
 54     }
 55     *elem = L.data[i-1];
 56     return 1;
 57 }
 58 // 打印全部值
 59 int getAllEleFormList(MyList l){
 60     for (int i=0; i<l.length; i++) {
 61         printf("-- list的%d元素:%d --
",i,l.data[i]);
 62     }
 63     return 1;
 64 }
 65 
 66 // 删除
 67 int deleteList(MyList *L, int i, int *ele) {
 68     
 69     if (L->length==0 || i < 0 || i > L->length + 1) {//线性表不为空 / 位置合法
 70         exit(0);
 71     }
 72     *ele = L->data[i];
 73     for (int j = i; j<L->length; j++) {
 74         // 删除元素的位置开始,后面的元素依次向前一位
 75         L->data[j] = L->data[j+1];
 76     }
 77     --L->length;// 表长度-1
 78     return 1;
 79 }
 80 
 81 // 清空
 82 int clearList(MyList *l) {
 83     l->length = 0;
 84     return 1;
 85 }
 86 
 87 int main(int argc, const char * argv[]) {
 88     @autoreleasepool {
 89         // insert code here...
 90         NSLog(@"Hello, World!");
 91         
 92         
 93         MyList L;
 94         int status;
 95         
 96         // 初始化
 97         status = initList(&L);
 98         printf("初始化的线性表 length=%d
",L.length);// 0
 99         // 插入
100         for (int i=0; i<5; i++) {
101             status = insertList(&L,i,i+10);// 10 11 12 13 14
102         }
103         printf("线性表 length=%d
",L.length);// 5
104         // 取值
105         int ele;// 变化的数据用它的指针来指
106         status = getElemFrmList(L, 2, &ele);
107         printf("线性表的第2个元素=%d
",ele);// 11
108         getAllEleFormList(L);
109         // 删除元素
110         status = deleteList(&L, 3, &ele);
111         printf("线性表的删除的第3个元素是=%d length=%d
",ele,L.length);// 13
112         // 清空list 里的元素
113         status = clearList(&L);
114         printf("线性表 length=%d
",L.length);// 0        
115     }
116     return 200;
117 }

2. 线性表顺序存储结构的优缺点

优点:

  • 无需额外为表示表中元素之间的逻辑关系而增加额外的存储空间;
  • 可以快速的存取表中任一位置的元素。

缺点:

  • 插入和删除操作需要移动大量元素;
  • 当线性表长度变化较大时,难以确定存储空间的容量;
  • 造成存储空间碎片。

2、线性表的链式存储

我们已知顺序存储的最大缺点是插入删除时需要移动大量数据元素,这显然是要耗时的,如何解决呢?--> 链式存储结构

文章 数据结构与算法 0 中链表部分有通过简单图形介绍无头结点的链表结构。

1. 带有头结点的链表:

2. 代码示例

1)带有头结点的单链表的 创建初始化、插入、删除、取值、遍历:

  1 // 方便起见,类型 typedef 一下
  2 typedef struct Node {
  3     
  4     int data;
  5     struct Node *next;
  6 } Node;
  7 typedef struct Node * linkedList;
  8 
  9 // 初始化一个空链表
 10 int func_initLinkedList(linkedList *L) {
 11     
 12     *L = (linkedList)malloc(sizeof(Node));
 13     if (*L == NULL) {
 14         return 0;
 15     }
 16     (*L)->next = NULL;
 17     return 200;
 18 }
 19 
 20 // 单链表插入数据
 21 int func_insertLinkedList (linkedList *L, int i, int e) {
 22     
 23     linkedList p,s;
 24     p = *L;
 25     
 26     int j = 1;
 27 
 28     // 找到要插入位置的前一个结点
 29     while (p && j<i) {
 30        p = p->next;
 31         ++j;
 32     }
 33     if (!p || j>i) {// p不存在 或 插入位置不合法
 34         return 0;
 35     }
 36     
 37     // 建一个新节点
 38     s = (linkedList)malloc(sizeof(Node));
 39     s->data = e;
 40     // 新结点 s 的 next 指向 p的后继
 41     s->next = p->next;
 42     // p 的后继指向 新结点 s
 43     p->next = s;
 44     return 200;
 45 }
 46 
 47 // 取值
 48 int func_getElem(linkedList L, int i, int *e) {
 49     
 50     int j;
 51     linkedList p;
 52     p = L->next;
 53     j = 1;
 54     while (p && j<i) {
 55         p = p->next;
 56         j++;
 57     }
 58     if (!p || j>i) {// p不存在 或 插入位置不合法
 59         return 0;
 60     }
 61     *e = p->data;
 62     return 200;
 63 }
 64 
 65 // 遍历
 66 int func_allElem(linkedList L) {
 67     
 68     linkedList p;
 69     p = L->next;
 70     
 71     while (p) {
 72         printf("getall: %d
",p->data);/**
 73                                         getall: 9
 74                                         getall: 8
 75                                         getall: 7
 76                                         getall: 6
 77                                         getall: 5
 78                                         getall: 4
 79                                         getall: 3
 80                                         getall: 2
 81                                         getall: 1
 82                                         */
 83         p = p->next;
 84     }
 85     return 200;
 86 }
 87 // 删除结点
 88 int func_deleteNode(linkedList *L, int i, int *e) {
 89     
 90     linkedList p,s;
 91     int j = 1;
 92     p = (*L)->next;
 93     
 94     // 找到要删除结点的前一个结点
 95     while (p->next && j<(i-1)) {
 96         p = p->next;
 97         ++j;
 98     }
 99     if (!(p->next) || j>i-1) {// p不存在 或 删除位置不合法
100         return 0;
101     }
102     
103     s = p->next;// 要删除的结点 s
104     p->next = s->next;
105     
106     *e = s->data;
107     free(s);
108     
109     return 200;
110 }
111 
112 
113 // 链式存储的线性表
114 void func_linkedList (){
115     
116     linkedList L;
117     struct Node *L1;
118     func_initLinkedList(&L);
119     func_initLinkedList(&L1);
120 
121     for (int i = 1; i<10; i++) {
122         func_insertLinkedList(&L, 1, i);
123     }// L:9 8 7 6 5 4 3 2 1
124     
125     int e;
126     func_getElem(L,3,&e);
127     printf("e: %d
",e);// e: 7
128 
129     func_allElem(L);
130     
131     int e1;
132     // 删除第 4 个
133     func_deleteNode(&L, 4, &e1);
134     printf("e1: %d
",e1);// e1: 6
135     func_allElem(L);
136 
137 }

2)单链表头插法尾插法

 1 // 头插法 - 插入n个结点
 2 int func_addHead(linkedList *L,int n) {
 3     
 4     // 建立带头结点的单链表
 5     *L = (linkedList)malloc(sizeof(Node));
 6     (*L)->next = NULL;
 7     
 8     linkedList p;
 9     for (int i=0; i<n; i++) {
10         
11         p = (linkedList)malloc(sizeof(Node));
12         p->data = i;
13         p->next = (*L)->next;
14         (*L)->next = p;
15     }
16     return 200;
17 }
18 // 尾插法
19 int func_addTail (linkedList *L,int n) {
20     
21     // 建立带头结点的单链表
22     *L = (linkedList)malloc(sizeof(Node));
23     (*L)->next = NULL;
24     linkedList t;
25     t = *L;// t 为尾结点
26     
27     linkedList p;
28     for (int i=0; i<n; i++) {
29         p = (linkedList)malloc(sizeof(Node));
30         p->data = i;
31 
32         t->next = p;
33         t = p;
34     }
35     t->next = NULL;
36     
37     return 200;
38 }
39 
40 // 调用
41     printf("
");
42     // 头插法
43     linkedList L3;
44     func_addHead(&L3, 10);
45     func_allElem(L3);// 9 8 7 6 5 4 3 2 1 0
46 
47     printf("
");
48 
49     // 尾插法
50     linkedList L4;
51     func_addTail(&L4, 10);
52     func_allElem(L4);// 0 1 2 3 4 5 6 7 8 9

3)清空链表

 1 // 清空链表
 2 int func_clearLinkedList(linkedList *L){
 3     
 4     linkedList p,tmp;
 5     p = (*L)->next;// p 指向第一个结点
 6     while (p) {
 7         tmp = p->next;
 8         free(p);
 9         p = tmp;
10     }
11     (*L)->next = NULL;// 头结点指针域置为空
12     return 200;
13 }

以上。

原文地址:https://www.cnblogs.com/zhangzhang-y/p/13743181.html