数据结构和算法

一、数据结构和算法概述

1.1什么是数据结构?

官方解释:

数据结构是一门研究非数值计算的程序设计问题中的操作对象,以及他们之间的关系和操作等相关问题的学科。
大白话:
数据结构就是把数据元素按照一定的关系组织起来的集合,用来组织和存储数据

1.2数据结构分类

传统上,我们可以把数据结构分为逻辑结构和物理结构两大类

逻辑结构分类:

逻辑结构是从具体问题中抽象出来的模型,是抽象意义上的结构,按照对象中数据元素之间的相互关系分类。

  1. 集合结构:集合结构中数据元素除了属于同一个集合外,他们之间没有任何其他的关系。
  2. 线性结构:线性结构中的数据元素之间存在一对一的关系
  3. 树形结构:树形结构中的数据元素之间存在一对多的层次关系
  4. 图形结构:图形结构的数据元素是多对多的关系

物理结构分类:

逻辑结构在计算机中真正的表示方式(又称为映像)称为物理结构,也可以叫做存储结构。常见的物理结构有顺序存储结构、链式存储结构。

  1. 顺序存储结构:

把数据元素放到地址连续的存储单元里面,其数据间的逻辑关系和物理关系是一致的 ,比如我们常用的数组就是顺序存储结构。
顺序存储结构存在一定的弊端,就像生活中排时也会有人插队也可能有人有特殊情况突然离开,这时候整个结构都处于变化中,此时就需要链式存储结构。

  1. 链式存储结构:

是把数据元素存放在任意的存储单元里面,这组存储单元可以是连续的也可以是不连续的。此时,数据元素之间并
不能反映元素间的逻辑关系,因此在链式存储结构中引进了一个指针存放数据元素的地址,这样通过地址就可以找
到相关联数据元素的位置

1.3什么是算法?

官方解释:

算法是指解题方案的准确而完整的描述,是一系列解决问题的清晰指令,算法代表着用系统的方法解决问题的策略
机制。也就是说,能够对一定规范的输入,在有限时间内获得所要求的输出。
大白话
根据一定的条件,对一些数据进行计算,得到需要的结果。

1.3.1算法的时间和空间复杂度

算法函数中n最高次幂越小,算法效率越高

  1. 算法函数中的常数可以忽略;
  2. 算法函数中最高次幂的常数因子可以忽略;
  3. 算法函数中最高次幂越小,算法效率越高。
描述增长的数量级说明举例
常数级别1普通语句两个数相加
对数级别logN二分策略二分查找
线性级别N循环找最大值
线性对数级别NlogN分治归并排序
平方级别N^2双层循环检查所有元素对

复杂程度从低到高依次为:

O(1)<O(logn)<O(n)<O(nlogn)<O(n2)<O(n3)
空间复杂度:一个字节是8位

1.3.2java中常见内存占用
  1. 基本数据类型内存占用情况:
数据类型内存占用字节数
byte1
short2
int4
long8
float4
double8
boolean1
char2
  1. 计算机访问内存的方式都是一次一个字节
  2. 一个引用(机器地址)需要8个字节表示:

例如: Date date = new Date(),则date这个变量需要占用8个字节来表示

  1. .创建一个对象,比如new Date(),除了Date对象内部存储的数据(例如年月日等信息)占用的内存,该对象本身也有内存开销,每个对象的自身开销是16个字节,用来保存对象的头信息。
  2. 一般内存的使用,如果不够8个字节,都会被自动填充为8字节:
  3. java中数组被被限定为对象,他们一般都会因为记录长度而需要额外的内存,一个原始数据类型的数组一般需要24字节的头信息(16个自己的对象开销,4字节用于保存长度以及4个填充字节)再加上保存值所需的内存。

二、排序算法

常用排序算法分析

时间复杂度空间复杂度
类别排序方法平均情况最好情况
插入类插入排序O(N^2) O(N)O(N^2)
希尔排序O(N^1.3-2)O(N)O(N^2)
选择类选择排序O(N^2)O(N^2) O(N^2)
堆排序O(NlogN)O(NlogN)O(NlogN)
交换类冒泡排序O(N^2) O(N)O(N^2)
快速排序O(NlogN)O(NlogN)O(N^2)
归并排序O(NlogN)O(NlogN)O(NlogN)
基数排序O(d(r+n)) O(d(n+rd))O(d(r+n))

2.1 Comparable接口介绍

Java提供了一个接口Comparable就是用来定义排序规则的,在这里我们以案例的形式对Comparable接口做一个简单的回顾。

需求:

  1. 定义一个学生类Student,具有年龄age和姓名username两个属性,并通过Comparable接口提供比较规则;
  2. 定义测试类StudentTest,在测试类StudentTest中定义测试方法Comparable getMax(Comparable c1,Comparable c2)完成测试
//学生类
public class Student implements Comparable<Student> {
    private String username;
    private int age;

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
    @Override
    public String toString() {
        return "Student{" +
                "username='" + username + '\'' +
                ", age=" + age +
                '}';
    }
    //定义比较规则
    @Override
    public int compareTo(Student o) {
        return this.getAge()-o.getAge();
    }
}

//测试类
public class StudentTest {
    public static void main(String[] args) {
        Student stu1 = new Student();
        stu1.setUsername("zhangsan");
        stu1.setAge(17);
        Student stu2 = new Student();
        stu2.setUsername("lisi");
        stu2.setAge(19);
        Comparable max = getMax(stu1, stu2);
        System.out.println(max);
    }
    //测试方法,获取两个元素中的较大值
    public static Comparable getMax(Comparable c1,Comparable c2){
        int cmp = c1.compareTo(c2);
        if (cmp>=0){
            return c1;
        }else{
            return c2;
        }
    }
}

2.2 冒泡排序

排序原理:

  1. 比较相邻的元素。如果前一个元素比后一个元素大,就交换这两个元素的位置。
  2. 对每一对相邻元素做同样的工作,从开始第一对元素到结尾的最后一对元素。最终最后位置的元素就是最大
    //冒泡排序
    public void bubbleSort(int[] arr) {
        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]) {
                    int temp = arr[j];
                    arr[j] = arr[j + 1];
                    arr[j + 1] = temp;
                    flag = false;
                }
            }
            if (flag) break;
        }
    }

2.3选择排序

排序原理:

  1. 每一次遍历的过程中,都假定第一个索引处的元素是最小值,和其他索引处的值依次进行比较,如果当前索引处
    的值大于其他某个索引处的值,则假定其他某个索引出的值为最小值,最后可以找到最小值所在的索引
  2. 交换第一个索引处和最小值所在的索引处的值
    //选择排序
    public void selectSort(int[] arr) {
        int index, min;
        for (int i = 0; i < arr.length; i++) {
            min = arr[i];
            index = i;
            for (int j = i + 1; j < arr.length; j++) {
                if (min > arr[j]) {
                    min = arr[j];
                    index = j;
                }
            }
            if (i != index) {
                arr[index] = arr[i];
                arr[i] = min;
            }
        }
    }

2.4插入排序

排序原理:

  1. 把所有的元素分为两组,已经排序的和未排序的;
  2. 找到未排序的组中的第一个元素,向已经排序的组中进行插入;
  3. 倒叙遍历已经排序的元素,依次和待插入的元素进行比较,直到找到一个元素小于等于待插入元素,那么就把待插入元素放到这个位置,其他的元素向后移动一位;
    //插入排序
    public void insertSort(int[] arr) {
        for (int i = 1; i < arr.length; i++) {
            int val = arr[i], j = i - 1;
            for (; j >= 0 && arr[j] > val; j--) {
                arr[j + 1] = arr[j];
            }
            arr[j + 1] = val;
        }
    }

2.5希尔排序

希尔排序是插入排序的一种,又称“缩小增量排序”,是插入排序算法的一种更高效的改进版本
排序原理:

  1. 选定一个增长量h,按照增长量h作为数据分组的依据,对数据进行分组;
  2. 对分好组的每一组数据完成插入排序;
  3. 减小增长量,最小减为1,重复第二步操作。
//希尔排序
public void shellSort(int[] arr) {
    for (int h= arr.length / 2; h> 0; h--) {
        for (int i = h; i < arr.length; i++) {
            int j = i, temp = arr[j];
            if (temp < arr[j - h]) {
                while (j - h>= 0 && temp < arr[j - gap]) {
                    arr[j] = arr[j - h];
                    j -= h;
                }
                arr[j] = temp;
            }
        }
    }
}

2.6 归并排序

2.6.1 递归

定义:

定义方法时,在方法内部调用方法本身,称之为递归.

注意事项:

在递归中,不能无限制的调用自己,必须要有边界条件,能够让递归结束,因为每一次递归调用都会在栈内存开辟 新的空间,重新执行方法,如果递归的层级太深,很容易造成栈内存溢出。

    //递归阶乘
    public static int factorial(int n){
        if (n==1){
            return 1;
        }
        return n*factorial(n-1);
    }
2.6.2 归并排序

归并排序是建立在归并操作上的一种有效的排序算法,该算法是采用分治法的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。

排序原理:

  1. 尽可能的一组数据拆分成两个元素相等的子组,并对每一个子组继续拆分,直到拆分后的每个子组的元素个数是1为止。
  2. 将相邻的两个子组进行合并成一个有序的大组;
  3. 不断的重复步骤2,直到最终只有一个组为止。
public void mergeSort(int[] arr, int left, int right, int[] temp) {
    if (left < right) {
        int mid = left + (right - left) / 2;
        mergeSort(arr, left, mid, temp);
        mergeSort(arr, mid + 1, right, temp);
        merge(arr, left, mid, right, temp);
    }
}

public void merge(int[] arr, int left, int mid, int right, int[] temp) {
    int i = left, j = mid + 1, t = 0, tempLeft;
    while (i <= mid && j <= right) {
        temp[t++] = arr[i] >= arr[j] ? arr[j++] : arr[i++];
    }
    while (i <= mid) {
        temp[t++] = arr[i++];
    }
    while (j <= right) {
        temp[t++] = arr[j++];
    }
    t = 0;
    tempLeft = left;
    while (tempLeft <= right) {
        arr[tempLeft++] = temp[t++];
    }
}

2.7快速排序

快速排序是对冒泡排序的一种改进。它的基本思想是:通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。

排序原理:

  1. 首先设定一个分界值,通过该分界值将数组分成左右两部分;
  2. 将大于或等于分界值的数据放到到数组右边,小于分界值的数据放到数组的左边。此时左边部分中各元素都小于或等于分界值,而右边部分中各元素都大于或等于分界值;
  3. 然后,左边和右边的数据可以独立排序。对于左侧的数组数据,又可以取一个分界值,将该部分数据分成左右两部分,同样在左边放置较小值,右边放置较大值。右侧的数组数据也可以做类似处理。
  4. 重复上述过程,可以看出,这是一个递归定义。通过递归将左侧部分排好序后,再递归排好右侧部分的顺序。当左侧和右侧两个部分的数据排完序后,整个数组的排序也就完成了。

切分原理:

把一个数组切分成两个子数组的基本思想:

  1. 找一个基准值,用两个指针分别指向数组的头部和尾部;
  2. 先从尾部向头部开始搜索一个比基准值小的元素,搜索到即停止,并记录指针的位置;
  3. 再从头部向尾部开始搜索一个比基准值大的元素,搜索到即停止,并记录指针的位置;
  4. 交换当前左边指针位置和右边指针位置的元素;
  5. 重复2,3,4步骤,直到左边指针的值大于右边指针的值停止。
public void quickSort(int[] arr, int left, int right) {
    if (left >= right) return;
    int i = left - 1, j = right + 1, mid = arr[(left + right) /2], temp;
    while (i < j) {
        do i++; while (i < j && arr[i] < mid);
        do j--; while (i < j && arr[j] > mid);
        if (i < j) {
            temp = arr[i];
            arr[i] = arr[j];
            arr[j] = temp;
        }
    }
    quickSort(arr, left, j);
    quickSort(arr, j + 1, right);
}

三、线性表

线性表是最基本、最简单、也是最常用的一种数据结构。一个线性表是n个具有相同特性的数据元素的有限序列。

前驱元素: 若A元素在B元素的前面,则称A为B的前驱元素
后继元素: 若B元素在A元素的后面,则称B为A的后继元素

  • 线性表的特征:数据元素之间具有一种“一对一”的逻辑关系。
    1. 第一个数据元素没有前驱,这个数据元素被称为头结点;
    2. 最后一个数据元素没有后继,这个数据元素被称为尾结点;
    3. 除了第一个和最后一个数据元素外,其他数据元素有且仅有一个前驱和一个后继。

线性表的分类:
线性表中数据存储的方式可以是顺序存储,也可以是链式存储,按照数据的存储方式不同,可以把线性表分为顺序表链表

3.1 顺序表

  1. 顺序表是在计算机内存中以数组的形式保存的线性表,线性表的顺序存储是指用一组地址连续的存储单元,依次存储线性表中的各个元素、使得线性表中再逻辑结构上响铃的数据元素存储在相邻的物理存储单元中,即通过数据元素物理存储的相邻关系来反映数据元素之间逻辑上的相邻关系。
  1. java中ArrayList集合的底层也是一种顺序表,使用数组实现,同样提供了增删改查以及扩容等功能。

get(i):不难看出,不论数据元素量N有多大,只需要一次eles[i]就可以获取到对应的元素,所以时间复杂度为O(1);
insert(int i,T t):每一次插入,都需要把i位置后面的元素移动一次,随着元素数量N的增大,移动的元素也越多,时间复杂为O(n);
remove(int i):每一次删除,都需要把i位置后面的元素移动一次,随着数据量N的增大,移动的元素也越多,时间复杂度为O(n);

3.2 链表

链表是一种物理存储单元上非连续、非顺序的存储结构,其物理结构不能只管的表示数据元素的逻辑顺序,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。链表由一系列的结点(链表中的每一个元素称为结点)组成,结点可以在运行时动态生成。

3.2.1 单向链表

单向链表是链表的一种,它由多个结点组成,每个结点都由一个数据域和一个指针域组成,数据域用来存储数据,指针域用来指向其后继结点。链表的头结点的数据域不存储数据,指针域指向第一个真正存储数据的结点。

3.2.2 双向链表

双向链表也叫双向表,是链表的一种,它由多个结点组成,每个结点都由一个数据域和两个指针域组成,数据域用来存储数据,其中一个指针域用来指向其后继结点,另一个指针域用来指向前驱结点。链表的头结点的数据域不存储数据,指向前驱结点的指针域值为null,指向后继结点的指针域指向第一个真正存储数据的结点。

java中LinkedList集合也是使用双向链表实现,并提供了增删改查等相关方法

3.2.3 链表的复杂度分析

get(int i):每一次查询,都需要从链表的头部开始,依次向后查找,随着数据元素N的增多,比较的元素越多,时间复杂度为O(n)

insert(int i,T t):每一次插入,需要先找到i位置的前一个元素,然后完成插入操作,随着数据元素N的增多,查找的元素越多,时间复杂度为O(n);

remove(int i):每一次移除,需要先找到i位置的前一个元素,然后完成插入操作,随着数据元素N的增多,查找的元素越多,时间复杂度为O(n)

相比较顺序表,链表插入和删除的时间复杂度虽然一样,但仍然有很大的优势,因为链表的物理地址是不连续的,它不需要预先指定存储空间大小,或者在存储过程中涉及到扩容等操作,同时它并没有涉及的元素的交换。

相比较顺序表,链表的查询操作性能会比较低。因此,如果我们的程序中查询操作比较多,建议使用顺序表,增删操作比较多,建议使用链表。

3.2.4 快慢指针

快慢指针指的是定义两个指针,这两个指针的移动速度一块一慢,以此来制造出自己想要的差值,这个差值可以然我们找到链表上相应的结点。一般情况下,快指针的移动步长为慢指针的两倍

3.2.5 循环链表

循环链表,顾名思义,链表整体要形成一个圆环状。在单向链表中,最后一个节点的指针为null,不指向任何结点,因为没有下一个元素了。要实现循环链表,我们只需要让单向链表的最后一个节点的指针指向头结点即可。

3.2.6 约瑟夫问题

问题描述: 传说有这样一个故事,在罗马人占领乔塔帕特后,39 个犹太人与约瑟夫及他的朋友躲到一个洞中,39个犹太人决 定宁愿死也不要被敌人抓到,于是决定了一个自杀方式,41个人排成一个圆圈,第一个人从1开始报数,依次往后,如果有人报数到3,那么这个人就必须自杀,然后再由他的下一个人重新从1开始报数,直到所有人都自杀身亡为止。然而约瑟夫和他的朋友并不想遵从。于是,约瑟夫要他的朋友先假装遵从,他将朋友与自己安排在第16个与 第31个位置,从而逃过了这场死亡游戏> 。

问题转换:

41个人坐一圈,第一个人编号为1,第二个人编号为2,第n个人编号为n。

  1. 编号为1的人开始从1报数,依次向后,报数为3的那个人退出圈;
  2. 自退出那个人开始的下一个人再次从1开始报数,以此类推;
  3. 求出最后退出的那个人的编号。
public class Test{
    public static void main(String[] args) throws Exception {
        //1.构建循环链表
        Node<Integer> first = null;
        //记录前一个结点
        Node<Integer> pre = null;
        for (int i = 1; i <= 41; i++) {
        //第一个元素
            if (i==1){
                first = new Node(i,null);
                pre = first;
                continue;
            }
            Node<Integer> node = new Node<>(i,null);
            pre.next = node;
            pre = node;
            if (i==41){
        //构建循环链表,让最后一个结点指向第一个结点
                pre.next=first;
            }
        }
        //2.使用count,记录当前的报数值
        int count=0;
        //3.遍历链表,每循环一次,count++
        Node<Integer> n = first;
        Node<Integer> before = null;
        while(n!=n.next){
        //4.判断count的值,如果是3,则从链表中删除这个结点并打印结点的值,把count重置为0;
            count++;
            if (count==3){
        //删除当前结点
                before.next = n.next;
                System.out.print(n.item+",");
                count=0;
                n = n.next;
            }else{
                before=n;
                n = n.next;
            }
        }
        /*打印剩余的最后那个人*/
        System.out.println("最后"+n.item);
    }
    //结点类
    private static class Node<T> {
        //存储数据
        T item;
        //下一个结点
        Node next;

        public Node(T item,Node next) {
            this.item = item;
            this.next = next;
        }
    }
}

3.3 栈

3.3.1 栈概述

栈是一种基于先进后出(FILO)的数据结构,是一种只能在一端进行插入和删除操作的特殊线性表。它按照先进后出的原则存储数据,先进入的数据被压入栈底,最后的数据在栈顶,需要读数据的时候从栈顶开始弹出数据(最后一个数据被第一个读出来)。

我们称数据进入到栈的动作为压栈,数据从栈中出去的动作为弹栈

public class Stack<T>{
    private Node head;  //记录首结点
    private int N; //当前栈的元素个数
    
    private class Node {
        public T item;
        public Node next;

        public Node(T item, Node next) {
            this.item = item;
            this.next = next;
        }
    }
    
    public Stack() {
        this.head = new Node(null, null);
        this.N = 0;
    }

    public boolean isEmpty() {//判断栈是否为空,是返回true,否返回false
        return N == 0;
    }

    public int size() {//获取栈中元素的个数
        return N;
    }

    public void push(T t) { //向栈中压入元素;//记录首结点
        Node newNode = new Node(t, null);//创建压入的节点
        Node next = head.next; //存储头节点原来的下个节点
        head.next = newNode; //头节点指向新节点
        newNode.next = next; //新节点指向next
        N++;
    }

    public T pop() { //弹出栈顶元素
        if (head.next == null) {
            return null;
        }
        Node first = head.next;
        head.next = first.next;
        N--;
        return first.item;
    }
}

3.4 队列

队列是一种基于先进先出(FIFO)的数据结构,是一种只能在一端进行插入,在另一端进行删除操作的特殊线性表,它按照先进先出的原则存储数据,先进入的数据,在读取数据时先读被读出来。

public class Queue<T>{
    private Node head;  //记录首结点
    private int N;  //当前栈的元素个数
    private Node last;  //记录最后一个结点

    private class Node {
        public T item;
        public Node next;

        public Node(T item, Node next) {
            this.item = item;
            this.next = next;
        }
    }

    public Queue() {
        this.head = new Node(null, null);
        this.last = null;
        this.N = 0;
    }

    public boolean isEmpty() { //判断队列是否为空,是返回true,否返回false
        return N == 0;
    }

    public int size() { //获取队列中元素的个数
        return N;
    }

    public void add(T t) { //往队列中插入一个元素
        Node node = new Node(t, null);
        if (last == null) {
            last = node;
            head.next = last;
        } else {
            Node oldLast = last;
            last = node;
            oldLast.next = last;
        }
        N++;
    }

    public T pop() { //从队列中拿出一个元素
        if (isEmpty()) {
            return null;
        }
        Node next = head.next;
        head.next = next.next;
        N--;
        if (isEmpty()) {
            last = null;
        }
        return next.item;
    }
}

四、树

  • 4
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值