【JavaLearn】 # (9)数据结构和算法

1. 数据结构

1.1 数据结构概述

数据结构:相互间存在一种或多种特定关系的数据元素的集合。是组织并存储数据以便能够有效使用的一种专门格式。

反映一个数据的内部构成:一个数据由哪些成分数据构成,以什么方式构成,是什么结构。

逻辑结构:线性结构(一对一)、树状结构(一对多)、网状(图)结构(多对多)

存储结构:顺序存储、链式存储、索引存储、散列存储

  • 顺序存储结构:数组,一块连续的存储空间

  • 链式存储结构:不连续的存储空间,每个结点由数据域和指针域组成

  • 索引存储结构:除建立存储结点信息外,还建立附加的索引表(按某个规则排序,然后将要查找的元素顺着比较,定位到某个范围中,去对应的页中寻找,继续定位范围,到页中寻找)来标识结点的地址。
    在这里插入图片描述

  • 散列存储结构:hash,根据结点的关键字直接计算出该结点的存储地址,查询速度特别快(顺序存储+链式存储)
    在这里插入图片描述

1.2 线性表

n 个类型相同数据元素的有限序列

  • 数据类型相同
  • 顺序性

1.2.1 顺序存储结构

在内存中,分配连续的空间,只存储数据,不存储地址,逻辑顺序与在计算机中实际存储的顺序一致

  • 优点:(按照索引)随机存取效率高,获取任一元素,直接计算即可

    (索引是 i 的元素的地址 = 起始地址 + 每个元素大小 * 索引)

  • 缺点:添加、删除效率低下(需要大量的移动元素)

1.2.2 链式存储结构

在内存中,分配不连续的空间,不仅存储数据,也要存储地址,逻辑顺序和在计算机实际存储的物理顺序不一致

  • 优点:添加、删除效率高(不需要移动元素,只需移动指针即可)
  • 缺点:随机存取效率低

注意:按照内容查找元素,都需要逐个比较,效率都低

为了使程序更加简洁,对空表、非空表的情况以及对 0 号结点进行统一处理,通常在单链表的最前面添加一个结点,称为头结点(数据域不存数据,指针域存储 0 号元素所在的结点地址)

1.2.3 其他链表

双向链表

前驱指针指向前一个结点,后继指针指向下一个结点,数据域存储数据。这样遍历数据时,既可以从头结点开始,也可以从尾结点开始

Java中的 LinkedList 底层就是双向链表

循环单链表

单向链表的最后一个结点的指针域,由之前的 null,改为指向第一个结点,形成一个环

循环双链表

1.3 栈和队列

都是运算受限的线性表

1.3.1 栈

  • 后进先出(Last In First Out)的特点

  • 只允许在表的一端进行插入和删除操作,操作的那端叫做 栈顶(top),另一端叫做栈底(bottom)

  • 当栈中没有数据元素时,称为空栈

  • 向一个栈中插入元素称为 入栈 push,从一个栈中删除元素称为 出栈 pop

  • 存储结构可以是顺序存储,也可以是链式存储
    在这里插入图片描述

1.3.2 队列

  • 先进先出(First In First Out)的特点
  • 只能在表的一端进行插入,称为队尾(rear),在表的另一端进行删除,称为队首(front)
  • 向队尾插入元素称为入队,从队首取出元素称为出队

1.3.3 双端队列 deque

两端都可以进行进队和出队操作的队列

也可以进行限制,前端只能进,后端可以进、可以出,或者反过来等

1.4 树和图

1.4.1 树

树是 n 个结点(n >= 0)的有限集,可以是空树,也可以只有一个根节点,也可以有多个子树

其中 A 是根,其余的结点分成 3 个不相交的集合:T1 = {B,E,F},T2 = {C,G},T3 = {D,H,I,J},每个集合都构成一棵树,且都是 根A 的子树

结点的度:结点有几个子树,度就为几(A 的度为 3),度为 0 的结点称为叶子结点(E、F、G…)

树的度:结点的度的最大值,结点的度最大为 3,那么树的度也为 3

各个结点的关系:父亲、儿子、兄弟、祖先、子孙、堂兄弟

1.4.2 二叉树

每个结点的度均不超过 2有序树

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ho795GZa-1625906051897)(C:\Users\lwclick\Desktop\Snipaste_2021-06-16_21-17-34.png)]

子结点在左边位置和在右边位置代表的是不一样的树。

满二叉树

高度为 k(第一层 k 为 0),并且有 2 ^ (k+1) - 1 个结点的二叉树,每一层的结点都是满的

完全二叉树

在一棵满二叉树的基础上,去掉右边部分的叶子结点,得到的二叉树,满二叉树必定是完全二叉树,反之不一定

二叉树的存储结构

可以使用顺序存储,也可以使用链式存储,更多的使用链式存储结构

在二叉树中,中间结点的孩子都是两个,则可以设计每个结点至少包括 3 个域:数据域、左孩子域、右孩子域

数据域存放数据元素,左孩子域存放指向左孩子结点的指针,右孩子域存放指向右孩子的指针,这样的二叉树存储结构称为 二叉链表
image-20210617210921249
二叉链表,通过父亲找孩子好找,但是通过孩子找父亲不好找,所以再增加一个指针域,指向结点的父结点,得到的存储结构称为 三叉链表

image-20210617211807836

1.4.3 查找树

二叉查找 / 排序树 binary search / sort tree (BST):

  • 首先是一棵二叉树
  • 若它的左子树不空,则左子树上所有结点的值小于它的根节点的值
  • 若右子树不为空,则右子树上所有结点的值大于它的根节点的值
  • 它的左、右子树也分别为二叉排序树
  • 对二叉查找树,进行中序遍历,得到有序集合
    image-20210617213019702

平衡二叉树 Self-balancing binary search tree (AVL树

  • 首先是一棵二叉查找树
  • 它的左右两个子树的高度差(平衡因子)的绝对值不超过 1,即每个结点的平衡因子都为 1、-1、0 的二叉查找树
  • 左右两个子树都是一颗平衡二叉树
  • 平衡二叉树必定是二叉查找树,反之不一定
  • 平衡因子:结点的左子树高度 - 右子树高度
  • 目的:减少二叉查找树层次,提高查找速度
    image-20210617233635962

红黑树 Red-Black Tree

  • 首先是一棵平衡二叉树
  • 每个结点或者是黑色,或者是红色
  • 根节点是黑色的,每个叶子结点(NIL)是黑色的(这里的叶子结点是指为空 null 或 NIL 的)
  • 如果一个结点是红色的,则它的子结点必须是黑色的
  • 从一个结点到该结点的子孙结点的所有路径上包含相同数目的黑结点

用它来存储有序的数据,时间复杂度是 O(logN),Java集合中的 TreeSetTreeMap 是用红黑树实现的
image-20210617234545705

1.4.4 图

多对多的关系,分为有向图无向图,无向图实际上也是有向图,是双向图
image-20210617234701882

加权图

权可能代表各种信息(一个点到另一个点的距离、消耗等),可以找权值最小的路线
image-20210617234914346

图的存储结构

可以使用顺序存储也可以使用链式存储,更多采用链式存储,称为邻接表

2. 算法

2.1 排序

将一个数据元素的任意序列,重新排列成一个按关键字有序的序列
image-20210619140603233

2.1.1 冒泡排序

  • 整个数列分为两部分:前面是无序的,后面是有序的
  • 初始状态下:整个数列都是无序的,有序数列是空的
  • 如果一个数列有 n 个元素,则至多需要 n - 1 趟循环才能保证数列有序
  • 每一趟循环可以让无序数列中最大数排到最后(也就是有序数列的元素个数 + 1)
  • 每一趟循环都从数列的第一个元素开始进行比较,依次比较相邻的两个元素,比较到无序数列的末尾即可
  • 如果前一个大于后一个,交换
    image-20210619141848269
public static void bubbleSort(int[] arr) {
    int tmp;
    // 大循环,有 n 个数,至多循环 n - 1 趟
    for (int i = 0; i < arr.length - 1; i++) {
        // 先假设数列是有序的
        boolean flag = true;

        // 小循环,每趟循环,数需要比较多少次,只需要比较到无序数列的末尾即可
        for (int j = 0; j < arr.length - 1 - i; j++) {
            if (arr[j] > arr[j + 1]) {
                tmp = arr[j];
                arr[j] = arr[j + 1];
                arr[j + 1] = tmp;
                // 如果发生了交换,说明是无序的
                flag = false;
            }
        }

        // 一趟循环结束后,判断下是否有序
        if (flag) { // 有序就直接退出
            break;
        }
    }
}

2.1.2 选择排序

  • 整个数列分为两部分:前面是有序数列,后面是无序数列
  • 初始状态下,整个序列都是无序的,有序数列是空
  • 一共 n 个 数,需要 n - 1 趟循环(一趟也不能少)
  • 每比较完一趟,有序数列数量 + 1,无序数列数量 - 1
  • 每趟先假设无序数列的第 1 个元素整个数列的第 i 个元素)是最小的,记下这个最小数(最小元素的索引),无需记最小元素的值,然后将最小的元素与第 i + 1 个元素开始比较,一直比较到最后一个元素(第 n 个)。如果发现更小的数,就将更小的数的索引作为最小数
  • 一趟比较完后,使用索引将发现的最小数无序数列的第一个数交换(如果最小数不是无序数列的第一个数)
    image-20210619150647715
public static void selectSort(int[] arr) {
    int minIndex, tmp;
    // 大循环,必须进行 n - 1 趟
    for (int i = 0; i < arr.length - 1; i++) {
        // 假设无序数列的第一个数是最小的,整个序列的第 i 个元素
        minIndex = i;
        // 小循环,将最小的数,与第 i + 1 个元素比较
        for (int j = i + 1; j < arr.length; j++) {
            if (arr[j] < arr[minIndex]) {
                minIndex = j;
            }
        }
        // 看minIndex的值是否发生变换
        if (minIndex != i) {
            tmp = arr[i];
            arr[i] = arr[minIndex];
            arr[minIndex] = tmp;
        }
    }
}

2.1.3 快速排序

  • 在需要排序的数中,选择第一个数基准数
  • 将这个序列中,所有比基准数大的放到基准数的右边,比基准数小的放到左边
    • 从初始序列的两端开始探测,刚开始的时候,让 i 指向序列的最左边 (i = 0),j 指向序列的最右边(j = n)
    • 因为基准数在最左边,所以先从右向左(j --)找一个比基准数小的数,然后再从左向右找一个比基准数大的数,交换它们
    • 然后 j 继续向左移动(j --),找到比基准数小的就停下,i 继续向右移动,找到比基准数大的停下,再次交换
    • 当 i 和 j 停下的位置是同一个元素时(i == j),将基准数停止位置的元素进行交换
    • 此时,基准数左边的元素全部比它小,右边的元素全部比它大,然后再分别处理左、右两边的区间即可
      在这里插入图片描述
/**
  * 快速排序
  * @param arr 要排序的数组
  * @param low 开始时最左边的索引
  * @param high 开始时最右边的索引
  */
public static void quickSort(int[] arr, int low, int high) {
    // 递归结束的条件
    if (low > high) {
        return;
    }
    
    // 开始时,让 i 指向最左边,j 指向最右边
    int i = low;
    int j = high;

    int tmp; // 交换用的临时变量

    // 设置基准位
    int baseNum = arr[low];

    // 循环开始排序,结束的条件是 i = j 时,即 i 和 j 相遇时
    while (i < j) {
        // 先 从右往左 找比基准数小的,同时要保证 i < j,找到就结束循环
        while (baseNum <= arr[j] && i < j) {
            j--;
        }

        // 再 从右往左 找比基准数大的
        while (baseNum >= arr[i] && i < j) {
            i++;
        }

        // i 和 j 找到各自的位置时,如果还没碰头,即 i < j,那么就交换
        if (i < j) {
            tmp = arr[i];
            arr[i] = arr[j];
            arr[j] = tmp;
        }
    }

    // 循环结束,i 和 j 相遇了,将相遇位置的元素和基准进行交换
    arr[low] = arr[i];
    arr[i] = baseNum;

    // 此时,基准数所在的位置,左边都是小于它的,右边都是大于它的; 再用同样的方式处理左右两边即可
    quickSort(arr, low, j - 1);
    quickSort(arr, j + 1, high);
}

2.1.4 插入排序

  • 一般称为直接插入排序,对于少量元素的排序,是一个有效的算法。

  • 将无序序列中的数据插入到有序的序列中。

  • 在遍历无序序列时,首先拿无序序列中的首元素去与有序序列中的每一个元素比较并插入到合适的位置,一直到无序序列中的所有元素插完为止。
    image-20210710151456437

public static void insertSort(int[] arr) {
    int tmp;
    // 外层循环,控制第几个元素进行插入
    for (int i = 1; i < arr.length; i++) { // i 从1开始,因为第一个元素自己有序
        // 内层循环,控制无序区中的首个元素,插入到有序区中,要进行几次比较
        for (int j = i; j > 0; j--) {
            if (arr[j] < arr[j - 1]) {
                tmp = arr[j];
                arr[j] = arr[j - 1];
                arr[j - 1] = tmp;
            } else {
                break;
            }
        }
    }
}

2.1.5 总结

  • 冒泡排序最多需要 n - 1 趟循环,最少需要 1 趟循环;选择排序必须进行 n - 1 趟

  • 冒泡排序中,最多的操作就是比较和交换,一趟循环可能发生多次交换;选择排序中最多的操作是比较,一趟比较结束后,如果发现了更小的值才交换一次

  • 对象的比较:应该让相应的类 实现 Comparable 接口并调用 compareTo() 进行比较

    public static void selectSort(Comparable[] arr) {
        int minIndex;
        Comparable tmp;
        // 大循环,必须进行 n - 1 趟
        for (int i = 0; i < arr.length - 1; i++) {
            // 假设无序数列的第一个数是最小的,整个序列的第 i 个元素
            minIndex = i;
            // 小循环,将最小的数,与第 i + 1 个元素比较
            for (int j = i + 1; j < arr.length; j++) {
                if (arr[minIndex].compareTo(arr[j]) > 0) {
                    minIndex = j;
                }
            }
            // 看minIndex的值是否发生变换
            if (minIndex != i) {
                tmp = arr[i];
                arr[i] = arr[minIndex];
                arr[minIndex] = tmp;
            }
        }
    }
    

2.2 递归

自己调用自己。计算通式,设置好退出条件。

缺点:占用内存多,速度慢。 优点:代码编写简单

特点:一个问题可以被分解为若干层简单的子问题。 子问题和其上层问题的解决方案一致。 外层问题的解决依赖于子问题的解决。

例子1:求 n !

/**
 * n! = n * (n - 1)!    n 的阶乘 为 n 乘 n-1 的阶乘
 * (n - 1)! = (n - 1) * (n - 2)!
 * .......
 */
public static long fac(int n) {
    if (n > 1) {
        return n * fac(n - 1);
    } else { // n为 1时,阶乘为 1,递归的结束条件
        return 1;
    }
}

例子2:求斐波那契数列的第 n 项

public static int fibo(int n) {
    if (n > 2) {
        return fibo(n - 1) + fibo(n - 2);
    } else {
        return 1;
    }
}

2.3 折半查找

又称为二分查找,必须满足:顺序存储结构必须有序排列

  • 初始时,low 为初始元素的下标, high 为末端元素的下标
  • 求 mid 的值: (high + low) / 2 下取整,取下标为 mid值的元素,去和要查找元素比较
  • 如果查找的值 大于 mid值的元素,那么要查找的元素在 [mid, high] 的区间中
    • low 变为 mid + 1,high 不变,继续重复
  • 如果查找的值 小于 mid值的元素,那么要查找的元素在 [low, mid] 的区间中
    • high 变为 mid - 1,low不变,继续重复
  • 直到 high < low
public static int BinarySearch(int[] arr, int low, int high, int key) {
    int mid = (low + high) / 2;
    if (low > high) {
        return -1;
    } else {
        if (arr[mid] > key) {
            return BinarySearch(arr, low, mid - 1, key);
        } else if (arr[mid] < key) {
            return BinarySearch(arr, mid + 1, high, key);
        } else {
            return mid;
        }
    }
}

非递归实现

public static int BinarySearch(int[] arr, int key) {
    int low = 0;
    int high = arr.length - 1;
    while (low < high) {
        int mid = (high + low) / 2;
        if (arr[mid] == key) {
            return mid;
        } else if (arr[mid] > key) {
            high = mid - 1;
        } else {
            low = mid + 1;
        }
    }
    return -1;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

LRcoding

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值