learning_record_doc/计算机/数据结构/数据结构原理及分析考点.md
2023-11-29 23:14:29 +08:00

21 KiB
Raw Blame History

数据结构原理及分析考点

线性数据结构

数组

  • 数组是一种线性数据结构,可以容纳固定数量的元素。

  • 数组中的每个元素都有固定的索引,这是访问数组元素的一种有效方式。

  • 数组的大小在创建时确定,不能动态扩展或缩小。

  • 数组在内存中是连续的,这使得某些操作(如随机访问)非常快速。

  • 但是,如果需要动态调整大小,可能需要创建新的数组并复制数据,这是不高效的。

链表

  • 链表是一种线性数据结构,可以动态添加或删除元素。
  • 每个元素包含数据和一个指向下一个元素的指针。
  • 链表不需要连续的内存空间,因此可以高效地进行插入和删除操作。
  • 但是,由于需要额外存储指针信息,因此链表的存储空间利用率可能较低。
  • 访问链表中的元素通常需要从头开始遍历,因此速度较慢。

1单链表

单链表单向链表只有一个方向结点只有一个后继指针next指向后面的节点。我们习惯性地把第一个结点叫作头结点链表通常有一个不保存任何值的head节点头结点通过头结点我们可以遍历整个链表。尾结点通常指向null。

2循环链表

循环链表其实是一种特殊的单链表和单链表不同的是循环链表的尾结点不是指向null而是指向链表的头结点。

3双向链表

双向链表包含两个指针一个prev指向前一个节点一个next指向后一个节点。

4双向循环链表

双向循环链表最后一个节点的next指向head而head的prev指向最后一个节点构成一个环。

队列

  • 队列是一种先进先出FIFO的数据结构通常用于管理需要按特定顺序处理的任务。
  • 队列中的元素只能从一端(队尾)添加,从另一端(队首)删除。
  • 队列的主要操作是入队(添加元素)和出队(删除元素),以及检查队列是否为空或已满。
  • 队列通常在内部维护一个双向链表实现其操作。

队列是先进先出的线性表。在具体应用中通常用链表或者数组来实现用数组实现的队列叫作顺序队列用链表实现的队列叫作链式队列。队列只允许在后端rear进行插入操作也就是入队enqueue在前端front进行删除操作也就是出队dequeue。

  • 栈是一种后进先出LIFO的数据结构通常用于跟踪正在进行的任务或操作的层次结构。
  • 栈中的元素只能从顶部添加和删除。
  • 栈的主要操作是push添加元素和pop删除元素以及检查栈是否为空或已满。
  • 栈也可以用数组或内部维护的链表来实现。

栈按照后进先出的原理运作。在栈中push和pop的操作都发生在栈顶。 用数组实现的栈叫作顺序栈,用链表实现的栈叫作链式栈。

非线性数据结构

二维数组

在一维数组中,每个元素也是一个数组元素,这样的数组称为二维数组。也可以理解为,二维数组是一个特殊的一维数组,这个一维数组的每个元素都是一维数组

多维数组

多维数组:多维数组是指超过二维的数组,通常在数学和计算机科学中用于表示高维空间数据和算法

二叉树

image-20231010215053315

image-20231010215027946

image-20231010214602780

image-20231010214614892

heap就是用数组实现的二叉树它没有使用父指针或者子指针。

堆根据” 堆属性 “来排序,该属性决定了树中节点的位置。

堆的应用:

  • 构建优先队列
  • 堆排序
  • 快速找到集合中的极值

什么是堆属性?

堆的定义是:一个数据集合 k={k0k1k2...kn},把它的所有元素按照完全二叉树的顺序储存方式存储在一个一维数组中,且满足下面的性质:

  1. 每个节点的值总是不大于或者不小于其父节点的值
  2. 是一棵完全二叉树

有两种情况:

  1. 父节点都不小于子节点的值,称为最大堆
  2. 父节点都不大于子节点的值,称为最小堆

注意,还需要是一棵完全二叉树,即除了最后一层节点的度不为 2其余每个节点的度都是 2如果最后一层的节点度不是 2那么要求左满右不满。

下面举个例子,哪些是最大堆,哪些是最小堆。

img

也就是最大堆从上往下数值减小,最小堆从上往下是数值变大,但同一层的节点的数值没有固定顺序。

这个要与二叉搜索树区别开,二叉搜索树中,左节点必须小于父节点,右节点必须大于父节点。

根据最大堆和最小堆的属性,可以快速访问到最值。

如何理解:用数组实现的树

在堆的定义中,说到” 所有元素按照完全二叉树的顺序储存方式存放在一个一维数组中 “,这里的顺序存储是什么意思?

顺序存储在物理上就是一个数组,但在逻辑上,我们理解为一颗二叉树,准确地说,应该是完全二叉树。

我们前面说到,堆可以表示为完全二叉树,这是相当节省空间的,为什么不表示为普通二叉树呢?

img

上图说明,如果将数组表示为非完全二叉树,则顺序储存时会造成空间的浪费,有些地址没有值。这就是为什么堆需要完全二叉树。此外,完全二叉树表示为数组时,父节点总是在子节点前面。

所以,堆总是具有这样的形状:

img

在树中的每个节点都满足堆属性。

广义表

广义表也称列表lists是线性表的扩展是一种可以嵌套的数据结构。广义表中的数据元素不仅有元素或者节点的名字而且有元素或者节点的值并且广义表可以共享也就是说一个广义表可以被其他的广义表共享12。

广义表的定义和性质如下:

  • 广义表的长度定义为最外层包含元素个数。
  • 广义表的深度定义为该广义表展开后所含括号的重数。其中原子的深度为0空表的深度为1。
  • 广义表可以为其他表共享,被共享表的值可以不必列出,而是通过名称引用。
  • 广义表可以是一个递归的表。递归表的深度是无穷的值,长度是有限值。

广义表Lists又称列表是线性表的推广。广义表是n(n≥0)个元素a1,a2,a3,…,an的有限序列其中ai或者是原子项或者是一个广义表。若广义表LSn>=1)非空则a1是LS的表头其余元素组成的表(a2,…an)称为LS的表尾。广义表的元素可以是广义表也可以是原子广义表的元素也可以为空。表尾是指除去表头后剩下的元素组成的表表头可以为表或单元素值。所以表尾不可以是单个元素值。

例子:

A=——A是一个空表其长度为零。

B=e——表B只有一个原子eB的长度为1。

C=a,(b,c,d))——表C的长度为2两个元素分别为原子a和子表(b,c,d)。

D=ABC——表D的长度为3三个元素都是广义 表。显然将子表的值代入后则有D=(( ),(e),(a,(b,c,d)))。

E=a,E——这是一个递归的表它的长度为2E相当于一个无限的广义表E=(a,(a,(a,(a,…)))).

三个结论:

1.广义表的元素可以是子表,而子表的元素还可以是子表。由此,广义表是一个多层次的结构,可以用图形象地表示

2.广义表可为其它表所共享。例如在上述例4中广义表ABC为D的子表则在D中可以不必列出子表的值而是通过子表的名称来引用。

3.广义表的递归性

考点:

1.广义表是0个或多个单因素或子表组成的有限序列广义表可以是自身的子表广义表的长度n>=0所以可以为空表。广义表的同级元素(直属于同一个表中的各元素)具有线性关系

2.广义表的表头为空,并不代表该广义表为空表。广义表()和(())不同。前者是长度为0的空表对其不能做求表头和表尾的运算而后者是长度为l的非空表(只不过该表中惟一的一个元素是空表),对其可进行分解,得到的表头和表尾均是空表()

3.已知广义表LS((a,b,c),(d,e,f)),运用head和tail函数取出LS中原子e的运算是head(tail(head(tail(LS)))。根据表头、表尾的定义可知任何一个非空广义表的表头是表中第一个元素它可以是原子也可以是子表而其表尾必定是子表。也就是说广义表的head操作取出的元素是什么那么结果就是什么。但是tail操作取出的元素外必须加一个表——““。tail(LS)((d,e,f))head(tail(LS))=(d,e,f)tail(head(tail(LS)))=(e,f)head(tail(head(tail(LS))))=e。

4.二维以上的数组其实是一种特殊的广义表

5.在非空广义表中1、表头head可以是原子或者一个表 2、表尾tail一定是一个表 3.广义表难以用顺序存储结构 4.广义表可以是一个多层次的结构

排序

概念:

  • 所谓排序,就是把一堆杂乱的数据,排成升序或降序 (递增 / 增减)。
  • 排序稳定性: 假设一组数据 [1,2,9,5,5,6,8],进行升序排序后,两个 5 的相应位置不发生改变,即称为稳定的排序,否则就是不稳定排序。

  • 内部排序: 数据元素全部放在内存中进行排序。
  • 外部排序: 即将待排序的记录存储在外存中,排序时再把数据一部分一部分地调入内存进行排序,在排序过程中需要多次进行内存和外存之间地交换。

插入排序


思路:

  1. 默认为第一个元素自己是有序的,从第二个元素开始。
  2. 取出第二个元素 tmp往前进行比较。
  3. 若该元素比 tmp 大,则将该元素往后移一位,直到找到比 tmp 小的。
  4. 找到比 tmp 小于等于的元素后tmp 插入到该元素的下一位。
  5. 循环 2~4 步骤。

步骤具体实现:

  1. 定义下标 i ,遍历数组。默认 i 下标已经是有序的,把 i 下标元素存入 tmp。
  2. 定义 jj 从 i-1 的位置,开始向前遍历,遇到比 tmp 大的元素,就把此时 j 下标的元素往后移一位,直到下标等于或小于 0 停止。
  3. j 下标元素小于 tmp则把 tmp 元素插入该 j 下标的下一个位置。

时间复杂度: O(N^2);

空间复杂度: O(1);

稳定性: 稳定;

具体代码实现:

public static void insertSort(int[] array){
    //1.遍历数组
    for (int i = 0; i < array.length; i++) {
        int tmp = array[i];
        //2.往前遍历,进行插入
        int j = i-1;
        for ( ;j >=0 ; j--) {
            if(array[j] > tmp){
                int exchange  = array[j];
                array[j+1] = array[j];
                array[j] = exchange;
            }else{
                //没有比tmp小的退出循环
                break;
            }
        }
        //3.此时j下标元素比tmp小tmp插入j下标的下一个位置
        array[j+1] = tmp;
    }
}

结论:

  1. 当数据趋于有序时,排序时间越快,最好的情况下时间复杂度为 O(N);
  2. 当把循环中 array[j] > tmp的大于号改为大于等于,此时就不是稳定的排序了。

希尔排序


思路:

  1. 先将待排序列进行预排序,使得待排序列接近有序,此时进行插入排序。
  2. 把待排序的数据分为多个组,每组间隔为 5 或 3…。
  3. 若此组的第一个元素大于最后一个元素,将此组第一个元素和最后一个元素交换。
  4. 重复上述操作,直到每组间隔只有 1 时,所有数据都在统一组内进行排好序。

步骤具体实现:

  1. 定义 gap = 数组长度 \ 2。
  2. 把待排序列分为 gap 个组,每个组的第一个元素和最后一个元素进行比较交换。
  3. 重复上述操作
  4. 当 gap 为 1 时,进行插入排序。

时间复杂度: O(N^1.3);

空间复杂度: O(1);

稳定性: 不稳定。

具体代码实现:

public static void shell(int[] array){
    int gap = array.length;
    while(gap > 1){
        gap /= 2;//分组
        //1.每组头和尾进行比较交换
        for (int i = 0; i < array.length; i++) {
            int tmp = array[i];
            //2.往前遍历一次步长为gap
            int j = i-gap;
            for ( ;j >=0 ; j-=gap) {
                if(array[j] > tmp){
                    int exchange  = array[j];
                    array[j+gap] = array[j];
                    array[j] = exchange;
                }else{
                    break;
                }
            }
            array[j+gap] = tmp;
        }

    }
}

结论:

  1. 希尔排序是对插入排序的优化。
  2. 当 gap>1 时,都是预排序,目的是为了让数组更趋于有序,当 gap==1 时,数组已经接近有序,这样进行插入排序就会很快。
  3. 希尔排序的时间复杂度不容易计算,因为 gap 的取值方法很多,导致很难计算,因此我们按照 Knuth 提出的时间复杂度 O(N^1.3) 来算。

选择排序


思路:

每次从待排序列中选择一个最小值 (最大), 存放在序列的起始位置,直到全部待排序的数据排完。

步骤具体实现:

  1. 定义i,假设待排序列i下标元素是最小值,用 min 记录当前i下标。
  2. 定义 ji下一个位置开始往后遍历,遇到小于array[min]时更新 minmin 指向每次遍历的最小值下标,直到遍历完一次数组。
  3. 一次遍历完后array[i]和 min 下标进行交换。
  4. 重复上述操作,直到待排序数据剩余 1 个元素。

时间复杂度: O(N^2);

空间复杂度: O(1);

稳定性: 不稳定;

具体代码实现:

public static void selectSort(int[] array){
    for (int i = 0; i < array.length; i++) {
        int min = i;
        for (int j = i+1; j < array.length; j++) {
            if(array[j] < array[min]){
                //更新min的值
                min = j;
            }
        }
        if(min != i){
            int tmp = array[i];
            array[i] = array[min];
            array[min] = tmp;
        }

    }
}

结论: 效率不是很好,实际中很少使用。

冒泡排序


思路: 根据序列中的两个记录键值比较结果来交换这两个记录在序列中的位置,交换排序的特点是:将键值较大的记录向序列的尾部移动,键值较小的向前部移动。

具体步骤实现:

  1. 定义i遍历数组,控制趟数,总体趟数比数组长度少 1
  2. 每趟让一个较大值移动到尾部。
  3. 定义j每次从 0 下标进行两两比较交换。

时间复杂度: O(N^2)

空间复杂度: O(1)

稳定性: 稳定;

具体代码实现:此处冒泡排序代码作了优化。

public static void bubbleSort(int[] array){
    for (int i = 0; i < array.length-1; i++) {
        boolean  flag = false;//判断此趟排序有没有交换
        for (int j = 0; j < array.length-1-i; j++) {
            if (array[j] > array[j + 1]) {
                int tmp = array[j];
                array[j] = array[j + 1];
                array[j + 1] = tmp;
                flag = true;
            }
        }
        //若flag一趟下来为false 说明数组已经有序
        if(flag == false){
            break;
        }
    }
}

快速排序

image-20231010214748182

查找算法

线性查找

定义

线性查找「 Linear Search」是一种最基础的查找方法其从数据结构的一端开始依次访问每个元素直到另一端后停止。线性查找实质上就是遍历数据结构 + 判断条件。比如,我们想要在数组nums中查找目标元素target的对应索引,那么可以在数组中进行线性查找。

代码实现

private static int sequenceSearch(int[] array,int target){
    for(int i=0;i<array.length;i++){
        if(target==array[i])
            return i;
    }
    return -1;
}

复杂度分析

时间复杂度 O(n):其中 n 为数组或链表长度。

空间复杂度 O(1):无需使用额外空间。

二分查找

定义

二分查找 「Binary Search」利用数据的有序性通过每轮缩小一半搜索区间来查找目标元素。

使用二分查找有两个前置条件:

  • 要求输入数据是有序的,这样才能通过判断大小关系来排除一半的搜索区间;
  • 二分查找仅适用于数组,而在链表中使用效率很低,因为其在循环中需要跳跃式(非连续地)访问元素。

代码实现

static  int binarySearch1(int arr[],int len,int target){
    /*初始化左右搜索边界*/
    int left=0,right=len-1;
    int mid;
    while(left<=right){
        /*中间位置:两边界元素之和/2向下取整*/
        mid=(left+right)/2;
        /*arr[mid]大于target即要寻找的元素在左半边所以需要设定右边界为mid-1搜索左半边*/
        if(target<arr[mid]){
            right=mid-1;
            /*arr[mid]小于target即要寻找的元素在右半边所以需要设定左边界为mid+1搜索右半边*/
        }else if(target>arr[mid]){
            left=mid+1;
            /*搜索到对应元素*/
        }else if(target==arr[mid]){
            return mid;
        }
    }
    /*搜索不到返回-1*/
    return -1;
}

复杂度分析

时间复杂度 O(logn):其中 n 为数组或链表长度;每轮排除一半的区间,因此循环轮数为 logn使用 O(logn) 时间。

空间复杂度 O(1):指针i,j使用常数大小空间。

哈希查找

定义

「哈希查找 Hash Searching」借助一个哈希表来存储需要的「键值对 Key Value Pair」我们可以在 O(1)时间下实现 “键→值” 映射查找,体现着 “以空间换时间” 的算法思想。

代码实现

public class HashSearch {

    /*待查找序列*/
    static int[] array = {13, 29, 27, 28, 26, 30, 38};
    /* 初始化哈希表长度此处哈希表容量设置的和array长度一样。
     * 其实正常情况下哈希表长度应该要长于array长度因为使用
     * 开放地址法时,可能会多使用一些空位置
     */
    static int hashLength = 7;
    static int[] hashTable = new int[hashLength];

    public static void main(String[] args) {
        /*将元素插入到哈希表中*/
        for (int i = 0; i < array.length; i++) {
        	insertHashTable(hashTable, array[i]);
        }
        System.out.println("哈希表中的数据:");
        printHashTable(hashTable);
        
        int data = 28;
        System.out.println("\n要查找的数据"+data);
        int result = searchHashTable(hashTable, data);
        if (result == -1) {
            System.out.println("对不起,没有找到!");
        } else {
            System.out.println("在哈希表中的位置是:" + result);
        }
    }

    /*将元素插入到哈希表中*/
    public static void insertHashTable(int[] hashTable, int target) {
        int hashAddress = hash(hashTable, target);

        /*如果不为0则说明发生冲突*/
        while (hashTable[hashAddress] != 0) {
            /*利用开放定址法解决冲突,即向后寻找新地址*/
            hashAddress = (++hashAddress) % hashTable.length;
        }

        /*将元素插入到哈希表中*/
        hashTable[hashAddress] = target;
    }

    public static int searchHashTable(int[] hashTable, int target) {
        int hashAddress = hash(hashTable, target);

        while (hashTable[hashAddress] != target) {
            /*寻找原始地址后面的位置*/
            hashAddress = (++hashAddress) % hashTable.length;
            /*查找到开放单元(未存放元素的位置)或 循环回到原点,表示查找失败*/
            if (hashTable[hashAddress] == 0 || hashAddress == hash(hashTable, target)) {
                return -1;
            }
        }
        return hashAddress;
    }

    /*用除留余数法计算要插入元素的地址*/
    public static int hash(int[] hashTable, int data) {
        return data % hashTable.length;
    }

    public static void printHashTable(int[] hashTable) {
    	for(int i=0;i<hashTable.length;i++)
    		System.out.print(hashTable[i]+" ");
    }
}

复杂度分析

时间复杂度 O(1):哈希表的查找操作使用 O(1) 时间。

空间复杂度 O(n):其中 n 为数组或链表长度。