数据结构-01:线性表

线性表


常见的线性表有数组和链表,为了更快速地查找元素,进阶学习跳表;静态链表可以作为了解,放在最后;

定义相关数据结构

线性表可以通过 ADT [Abstract Data Type] 进行一系列的封装,在C语言中可以使用 typedef 关键字将不同的数据类型封装到同一个数据结构中,例如:

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

typedef struct Student {
    char[10] id;		// 用于存储学生的学号信息
    char[20] name;		// 用于存储学生的姓名
    double[7] scores;	// 用于存储学生7个学科的成绩
} *stu, student;	// 这里使用 * 号是之后如果使用 stu 对变量进行赋值的话会直接视作指针,没有 * 号的 student 则视作普通变量
// 之后可以使用 malloc 函数动态分配一个 Student 数组的内存空间,用于存放多个 Student;

以上是使用数组初始化线性表,也可以使用链表:

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

typedef struct Student {
    char[10] id;		// 用于存储学生的学号信息
    char[20] name;		// 用于存储学生的姓名
    double[7] scores;	// 用于存储学生7个学科的成绩
    struct Student* next; 	// 用于保存下一个 Student 节点的内存地址
} *stu, student;

相对于 C 语言 这种面向过程的编程语言而言,它操作的数据如 int a = 10; 是将 10 作为一个数值存储在计算机内存块中,通过 a 这一个变量名直接可以获取该内存块中的值;而指针的内存块中存储的是另一个内存块的地址,通过这个地址可以找到指针所指向的地址存储的值;而 C 语言 的函数编写同样是一个全局的应用,即同一个程序中的任何一个地方都可以直接调用到其余函数,相对而言保密性低、耦合度高,对于稍大型的程序体系会产生不必要的阻碍;

进而发展出了例如 java, C++, C#, JavaScript, Python 等一系列面向对象[1]的编程语言,将程序的最小单元从之前的函数扩大到了对象这个新的概念上,通过封装、继承、多态这三大特性,完成了对于大型程序的支持度,降低了程序与程序之间的耦合度;

既然到了这儿,就先简单介绍一下什么是面向对象,而什么又是面向过程;简单而言,面向对象让你从专注于事务的准备、处理、完成、提交简化到了只需要专注于事务的发布;

例如,想吃一道鱼香肉丝,面向过程的行为就像是:自己去买材料、开火炒菜、关火出炉,然后再通知可以食用,自己再吃菜;对于过程更加看重;

而在面向对象的思想下,想吃鱼香肉丝的过程就是你去到饭店,对老板说:“来分鱼香肉丝。”然后只等上菜就行了。中间的材料准备、烧菜过程都不需要你操心,你只管调用另一个对象的方法达到你的目的。

使用 Java 编写上面的 C语言 代码如下,对应的变量前有访问权限限定词 private,表示只能被自己修改;

class Student {
    private String id;
    private String name;
    private double[7] scores;
}

线性表相关操作

在上面介绍了 ADT 数据结构的线性表的构建模式,通常而言线性表存放的是一列数字或者字符,所以可以用基本数据类型 intfloatdoublechar 对数据进行定义;在本章之后的内容中默认对数字的线性表进行操作;

上面还介绍了面向对象的编程语言,在本章节之后的一些算法题会使用高级编程语言实现,当然C语言也是可以的,但是就笔者而言,懂得如何造轮子是好事,但是同样应该学习如何使用已经造好的轮子,减少在不必要的内容上的过多纠结,专注于提升自己的技术水平更重要;

要了解线性表的使用,首先应该理解线性表的模型,在思维中建立一个具象化的形象,在使用线性表的时候就用脑中的形象去类比它的执行模式,可能前期会耗时更久,但是对于之后的学习会更容易纠错、发展和完善;

在笔者的思维中,线性表存在两种形式,一个是数组,一个是链表;数组在我脑中的形象是一个方块,方块由一个个更小的单元组成,在小单元里面通过 数组名+索引 获取,可以想象成一个监牢,每个牢房都顺序编号,我只要能知道一个牢房的宽等于多少步,我就可以闭着眼睛走到任意一个牢房门口;而链表则不同,它像一个猜谜的一个个线索,这些线索散布在不同的位置,需要找到前一个线索才有机会知道下一个线索的位置,这个时候完全不可以玩闭眼找房间的游戏,说不定我刚闭眼前面就是一个悬崖;

有了这种概念之后,就可以使用线性表完成相关的功能了;[我会提思路,但是不会怎么写代码]

1、一直两个线性表 ListAListB,其数据都是以递增方式有序排列,要将两个线性表合并入 ListC 中,并且要求 ListC 内容有序,怎么做?

首先,我们要知道 ListAListB 是那种线性表,如果数组,那就需要一个新的数组参与到运算中,根据上一章的空间复杂度预估这里的空间复杂度达到了 O(ListA.length() + ListB.length());暂且不表,看另一种,如果是链表的话,那我们甚至可以只申请三个节点就可以完成合并的目的;

其次,想一想我们的合并的方式,可以先合并数组在排序,也可以在合并数组的同时完成排序;先合并数组在排序的时间复杂度根据上一章,平均情况最好为 n = ListA.length() + ListB.length(), O(n log2n),这是基于我们掌握了堆排、快排或者归并排序中的任意一种的;如果边合并边排序的话,我们需要做的是使用判断两个线性表中的对应数的大小,再将满足条件的数放入新的线性表中,所以这里的时间复杂度大概为 O(n);需要对两个线性表两两进行判断,再进行数据插入;判断一个数据插入一个数据;

最后,可以确定使用边合并边排序的算法,那这里就考虑使用哪种数据结构具象化链表了:

/* C
* 当线性表是由数组实现的时候,合并两个线性表
* int[] listA: 传入的第一个数组
* int[] listB: 传入的第二个数组
* 返回值 int[]: 两个数组合并之后的数组
*/
int[] mergeArrays(int[] listA, int[] listB) {
    int lenA = length(listA), lenB = length(listB)
    int len = lenA + lenB;
    int[] listC = (int*)malloc(sizeof(int) * len);
    
    int i = 0, j = 0;
    while (i < lenA && j < lenB) {	// 不使用 for 循环是因为每一轮循环后的逻辑不好写,只需要小的那个数组对应的角标自增一个
        if (listA[i] < listB[j]) {	// 如果 i 对应的 listA 的数值比 j 对应的 listB 的数值更小,就把 listA[i] 放入 listC[i + j] 中
            listC[i + j] = listA[i++];	// 本来应该考虑 C 的角标再 +1,但是由于数组的计数本就是从 0 开始,所以就没有 +1
        } else {
            listC[i + j] = listB[j++];	// 这两个判断条件的括号可以删掉,因为只有一条语句
        }
    }
    
    if (i < lenA) {
        // 上述循环的推出条件是 且,所以要将最后没有插入的数继续插入,因为 i\j 一定有一个满足退出循环的条件,所以可以使用 if\else 语句
        while (i < lenA)
            listC[i + j] = listA[i++];
    } else {
        while (j < lenB)
            listC[i + j] = listA[j++];
    }
    
    return listC;	// 因为 listC 是数组名,是一个指针,可以指向数组,直接返回即可
}

/*
* 计算数组的长度
* int[] array: 待测的数组
* 返回值 int: 待测数组的长度
*/
int length(int[] array) {
    int count = 0;
    while (array[count++] != null);
    return --count;
}
/** java
* 当线性表是由数组实现的
* @author JEason
* @param listA: 线性表 A
* @param listB: 线性表 B
* @return 合并 A、B 之后的线性表
*/
public int[] mergeList(int[] listA, int[] listB) {
	int len = listA.length + listB.length;
    int[] listC = new int[len];
    ... 同上
        
    return listC;
}
/*	线性表用链表实现时的节点
class ListNode {
	int val;
	ListNode next;
} */

/** java
* 线性表用链表实现的
* @author JEason
* @param listA: 线性表 A
* @param listB: 线性表 B
* @return 合并 A、B 之后的线性表
*/
public ListNode mergeList(ListNode listA, ListNode listB) {
	ListNode head = new ListNode(listA.val + listB.val);	// 头节点的数据用于存储链表长度
	ListNode a = listA.next, b = listB.next, p = head, tmp = null;	// 设计两个指针移动,实现判断和添加
    head.next = tmp;
    
    while (a != null && b != null) {	// 使用节点为空作为终止条件
        // 因为是链表,所以可以不新建节点,直接移动节点过来就行
        if (a.val < b.val) {	// 先将小数节点挑选出来,用 tmp 作为指针接住节点,再将小数对应的节点移至它的下一个节点
            tmp = a;
            a = a.next;
        } else {
            tmp = b;
            b = b.next;
        }
        // 将小数节点放入目标链表中,并移动目标链表的尾指针
        p.next = tmp;
        p = p.next;
    }
    // 当 a\b 为空节点的时候,只需要把不为空的节点节点放入目标链表中即可
    if (a != null) p.next = a;
    else p.next = b;
    
    // 将 ListA 和 ListB 的大小归零
    listA.val = 0;
    listB.val = 0;
    
    return head;
}

小结

线性表的实现一般就是数组或者链表,熟练掌握数组和链表的特性,在遇到算法设计时根据需求做出相应的选择;

数组要注意它的开始位置和退出条件,退出之后判断因子 [如上述代码中的 i OR j] 应该是什么状态,可以在设计完代码之后详细思考一下;

链表要注意在移除节点或者添加结点的时候要保证不会有任何一个节点在操作过程中有遗失的风险;

一点拓展

跳表-点击跳转

跳表是一种数据结构。它允许快速查询一个有序连续元素的数据链表。跳跃列表的平均查找和插入时间复杂度都是O(log n),优于普通队列的O(n)。

跳表实际上就是用升维的方式简化数据,从而达到快速查询、插入和删除的功能;

静态链表-点击跳转

静态链表和单链表其实很相似,只是做的方法不同。单链表的一个节点会存放数据域和指针域,指针域存放下一个节点的指针地址。

静态链表分为数据与和游标,游标是记录下一个元素在数组中的位置,其实静态链表更像数组。

静态链表其实就是以高维数组[2]的方式表示对应的链表;

一些算法题

算法题并不可怕,只不过一般不推荐直接使用 C语言 做算法题,因为 C语言 有很多小组件需要自己写,倒不是很必要;但是如果使用 C++ / Java / Python 之类的编写算法呢,需要先了解相关的一些包,有很多数据结构它是直接封装好了只管使用的,工欲善其事必先利其器就是这个道理,不要怕困难,咬咬牙总会有开始轻松的时候!加油。

我会将下面的题目尽可能地编录到一起,新发一条博文,一起加油吧!

数组算法题 Java Array

链表算法题 Java LinkedList

随意练练

参考

菜鸟教程

数据结构之跳表 - fangjiaxiaobai - 掘金

简单的静态链表 - 小天秤 - 掘金

LeetCode

脚注:


  1. 面向对象编程:Object Oriented Programming,简称OOP; ↩︎

  2. 不一定是二位数组,可以是多为,主要依据链表指向其他节点的指针数量和本节点的数据量,即如果需要一个单链表 [一个节点数据 + 一个结点指针] 就需要二维,如果是双向链表 [一个节点数据 + 一个后置节点指针 + 一个前置节点指针] 则需要三维数组; ↩︎

posted @ 2020-12-27 10:43  JE_Chris  阅读(109)  评论(0)    收藏  举报