数据结构和算法入门

复杂度

大O记法

计算机怎么判断程序性能?

我们都知道编程基本上是在和数据打交道,大多数程序基本都在处理获取数据、查询数据、操作数据、返回数据相关的逻辑。

因此出现了数据结构和算法,这两者出现本质为了解决如何能够更快更省进行数据处理。

这里的更快,也就是程序运行时间更短,对应的指标我们称之为时间复杂度

这里的更省,也就是程序运行所消耗的内存更少,对应的指标我们称之为空间复杂度

为什么需要复杂度?

通过开始运行和结束运行这两个时间点差额和内存使用情况的计算方法可以判断性能,进行复杂度分析主要以下有三个原因:

1.测试结果大大依赖硬件条件

以时间举例,不可能绝对的说某个代码需要跑 10s,在某台机器上就能够跑 10s。因为程序的执行和机器的性能正向相关,如果放在一台老的机器上会大大多于 10s,而放在一款未来高性能机器上可能只需要 5s。 因此受硬件环境的影响,并不能简单的用时间去统计。

2.测试结果需要事后才能计算

上面所有的操作,往往都是在程序运行之后才能形成统计结果,但我们大多希望在伪码阶段,便能预测程序的执行性能。 因此这种事后统计法不能作为性能预测的方法。

3.测试结果受原始数据特性影响大

以排序为例计算需要所需的时间,试想如果给出的数据就已经是排好序的,那需要的时间几乎等于 0,而如果给出的数据是完全的打乱的,那需要时间肯定非常长。 因此发现测试结果本身就存在差异,除非我们的样本足够的多,否则计算出来的结果往往具有片面性

总结

所以我们需要一个不用具体的测试数据和测试环境,就可以粗略地估计算法执行效率的方法。 这个方法称作为复杂度也就是大O 记法。

复杂度分析(大 O 记法)是数据结构和算法的精髓和基石,只有掌握好它,才能更好的学习数据结构和算法。

时间复杂度

如何计算时间复杂度?

在现实中无法用实际运行时间预估程序运行时间,但是可以使用步数作为时间复杂度的计算。步数可以简单理解数组的每次索引值的读取,就算作一步,也可以被称作为unit_time

案例1:数组值获取(O(1))

题目:我们需要获取数组的第 4 个位置的值。

int a[] = {0, 1, 2, 3, 4, 5, 6};
System.out.println(a[3]); // step1

如上step1:获取a第 3 个索引位置的值,把这个时间称作为 1 步。 实际上在计算机底层,计算机可以通过一步跳到任意一个索引的位置进行数据的读取,因此这个时间也被称为单位时间unit_time

如果用大 O 记法,我们称作为常数时间---O(1)。 用通俗的话解释也就是:无论数组的长度多少,获取数组中某个值的都只是 1 步。随着数组数量增加,时间复杂度并不会提升。

如果用数学公式表示程序运行所需的时间(x 表示数组的长度,后面坐标图都一样),结果是:

案例2:猜数字游戏(O(logN))

在上一章,我们学习过的猜数字的游戏,我们知道需要的步数为log(N),我们称为对数时间---O(log(N))

用数学公式表示如下:

可以看到O(log(N))O(1)增长的更快一些性能更差一点

案例3:计算 N 的阶乘(O(N))

题目:我们需要定义一个函数,这个函数能计算传入参数 N 的阶乘 N!。

public int factorial(int n) {int result = 1; //step1for (int i = 1; i <= n; i++) { //step2result *= i;  // step3}return result;
}

这里就比上面复杂,如果对数据操作一次,算作一步,大致数一数一共需要多少步:

  • step1 : 初始化变量 result : 1 步;
  • step2 : 初始化变量 i 。1 步;i++ 相当于 i = i + 1,需要执行 N 次,因此 step2 一共 N + 1 步;
  • step3 : 执行 N 步。

(注意,在此处暂时只考虑数据赋值操作情况,暂时不考虑i <= n代码)

那么一共的时间复杂度为2N + 2。如果用大 O 记法,称作为**线性时间---O(2N + 2)**。

忽略常量

通过这两个案例,大家已经发现大 O 记法,表示的是代码执行时间随数据规模增长的变化趋势,也就是需要把 N 想象成 1W,100W 甚至更大。

而在这种情况下,常量、系数部分并不左右增长趋势,所以都可以忽略。

所以上面**O(2N + 2),通常我们会表示为O(N)**。

因此上面三个案例,用图示为:

再看一个更复杂的案例:

案例4:计算一个数组中的所有组合方式(O(N^2))

定义一个函数,能打印传入参数的两两组合情况。 例如:传入一个数组{"hi", "m", "a", "c"},里面有 4 个英文词语,两两组合,一共有哪些组合情况呢?

public void combine(String args[]) {for (int i = 0; i < args.length; i++) { //step1for (int j = i + 1; j < args.length; j++) { //step2System.out.println(args[i] + args[j]); //step3}}
}

对于上面的时间复杂度:

  • step1: int i = 0 1 步,i++ N 步,因此一共 N+1 步
  • step2: 分别执行 (N) + (N-1) + (N-2) + ... + 2 + 1 = N*(N + 1) / 2
  • step3: 执行了 N*(N - 1) / 2

最终的结果为 N^2 + N + 1步。 注意,此次有指数出现了,称为指数时间--O(N^2 + N + 1),忽略常数为O(N^2)**。对于指数时间而言,线性时间都可以被忽略,例如:O(N^2 + N + 1),最终忽略以后的结果为 O(N^2)

把三个案例放一起,对比时间复杂度的趋势(只关注关心右上角(X > 0 区域)):

总结

从上面 3 个案例我们可以总结出规律:大 O 记法,只保留最大趋势公式,指数 > 线性 > 对数 > 常数。因此在计算时间复杂度的时候,其实并不需要一行行看代码,只需要关注for 循环嵌套情况。写代码的时候,如果能用线性复杂度的代码,替换指数复杂度的代码,那就是大大性能优化。

空间复杂度

如何计算空间复杂度?

空间复杂度和时间复杂度一样,同样遵循大 O 记法。

时间复杂度是以步作为基础单位,空间复杂度是以一个基础数据类型值当做基础单位。

O(1)
int a = 0;
int j = 0;
O(N)
int a[] = new int[n]

优先考虑时间复杂度

大多数情况只会考虑时间复杂度,因为默认计算机内存足够大的,足够使用。 大家是否听说过摩尔定律

当价格不变时,集成电路上可容纳的元器件的数目,每隔 18-24 个月便会增加一倍,性能也将提升一倍。换而言之,每个 18-24 个月,同样大小的芯片里可存储的数据量将翻一倍。 因此完全可以忽略内存空间不够的情况。除非参与嵌入式开发,参与单片机相关的编程开发,才需要注意空间问题。

性能优化小案例1 - 二分查找

有序数组查找

如果一个数组中的值是按一定顺序进行排列的,就称为有序数组。比如数组[2, 3, 22, 75, 80, 90, 100]

如果希望完成一个函数,查找某个数字是否存在数组中。例如:22 存在数组中,索引值是 250 不存在数组中。

线性查找

同样给出最粗暴的解决方案,依次遍历整个数组,判断给定的数字是否存在。

public static int find(int[] array, int aim) {for (int i = 0; i < array.length; i++) {if (aim == array[i]) {return i;}}return -1;
}

这种查找方法的时间复杂度为:O(N)。但还有更快的查找办法:二分查找法

二分查找法

和猜数字游戏类似,因为数组是有序的,可以先比对中间索引值,再来缩小查找的范围。

怎么能让数组缩小呢

为了不破坏原数组(因为原数组可能还会在其他地方使用),可以定义两个变量 left, right 分别表示数组可筛选区域左侧和右侧的索引(下标)。

代码如下:

public static int find(int[] array, int aim) {// 初始化left = 最左侧, right = 最右侧int left = 0;int right = array.length - 1;// 当left > right则表示遍历完成while (left <= right) {// 获取中间的值int middle = (left + right) / 2;int middleValue = array[middle];if (middleValue == aim) {// 已经找到目标return middle;} else if (middleValue < aim) {// 目标在middle的右侧,重置leftleft = middle + 1;} else {// 目标在middle的左侧,重置rightright = middle - 1;}}return -1;
}
对比下列数据查找的时间复杂度:
暴力查找二分法查找
时间复杂度O(N)O(log(N))
数组 100 个1006.64
数组 1W 个1W13.28
数组 100W 个100W19.93

从上面可以看到,数组越大,数据量越大,最终性能提升相当的明显,相差了几个量级

小提示:在 Java 中可以使用String对象compareTo方法比较字符串的先后顺序,语法规则为

str1.compareTo(str2)

结果如果是负数则表示str1排序在str2前面

举例:

import java.util.ArrayList;
import java.util.Comparator;public class Address {public static int find(ArrayList<String> array, String aim) {// 初始化left = 最左侧, right = 最右侧int left = 0;int right = array.size();// 当left > right则表示遍历完成while (left <= right) {// 获取中间的值int middle = (left + right) / 2;String middleValue = array.get(middle);if (middleValue.equals(aim)) {// 已经找到目标return middle;} else if (middleValue.compareTo(aim) > 0) {// 目标在middle的左侧,重置rightright = middle - 1;} else {// 目标在middle的右侧,重置leftleft = middle + 1;}}return -1;}public static void main(String[] args) {ArrayList<String> array = new ArrayList<>();array.add("Allen");array.add("Emma");array.add("James");array.add("Jeanne");array.add("Kelly");array.add("Kevin");array.add("Mary");array.add("Natasha");array.add("Olivia");array.add("Rose");int result1 = find(array, "Kelly");if (result1 == -1) {System.out.println("Kelly 不存在名单中");} else {System.out.println("Kelly 存在名单中,位置是 " + result1);}int result2 = find(array, "Edith");if (result2 == -1) {System.out.println("Edith 不存在名单中");} else {System.out.println("Edith 存在名单中,位置是 " + result2);}}
}

性能优化小案例2 - 二次问题

假设给定一个数字数组,数组里面每个数字介于 0 - 10 之间,请找出里面重复的数字。

举个例子:

数字数组为:[0, 8, 2, 3, 5, 6, 2, 2, 10, 8]重复数字为:[8, 2]

暴力破解法

每次获取一个元素,依次判断这个元素和之后的元素是否有重复,图示如下:

第一步,我们选择第一个元素 0, 判断与后面 8 个元素是否相同。 第二步,我们选择第二个元素 8, 判断与后面 7 个元素是否相同。 ... 以此类推

代码如下:

public static ArrayList<Integer> repeat(int[] array) {ArrayList<Integer> result = new ArrayList<>();for(int i = 0; i < array.length; i++){// 以此判断i位置元素和后面j位置元素是否相等for(int j = i + 1; j < array.length; j++){if(array[i] == array[j]){// 如果之前result没有,则进行添加if(!result.contains(array[i])){result.add(array[i]);}}}}return result;
}

最终的运行结果,我们看到打印出[8, 2]和我们预期一样。

时间复杂度是多少呢?很容易算出来,两个for循环嵌套,所以是O(N^2)(此处我们还未计算containes的复杂度)。

标记法

再次阅读下题目的要求:

假设给定一个数字数组,数组里面每个数字介于 0 - 10 之间,请找出里面重复的数字。

从这个题目要求知道数组里面每个数字都是介于 0 - 10 之间。 我们是否可以先用11 长度的数组来标记 0 - 10 这 11 个数字是否存在;空 表示不存在,1 表示存在一次,2 表示存在 2 次

如下图所示:

假设现在有一个数组为0, 2, 3,那么exists数组中索引0, 2, 3位置会被标记成 1。

那怎么判断重复呢?

一旦出现一个数字就将exists对应索引位置的值 + 1。下次如果遇到对应的位置已经为 >= 1,则表示重复了。

详细步骤:

前置

首先申请一个数组,数组名称为exists,表示数字是否存在。 数组的长度为 11,并且数组里面的值默认都为 0,表示 0 - 10 这 11 个数默认都不存在。

第一步

第一步,我们扫描到数字0,因此我们将exists数组中索引值为 0 的数字设置为 1。

exists[0] = 1

第二步

第二步,我们扫描到数字8,因此我们将exists数组中索引值为 8 的数字设置为 1。

exists[8] = 1

以此类推

第六步

第六步的时候,我们的exists数组0, 2, 3, 5, 8索引位置已经设置为 1。

扫描到原始数组第 6 位为 2,我们发现exists数组中索引为 2 的位置已经为 1 了,所以我们知道 2 这个数字重复了。

然后将2写入result的数组中,并且将2对应的索引值改为 2,表示出现过 2 次了。

后面再出现 2 该怎么办呢

这时候不能继续往result里面添加元素了,所以我们需要判断对应的标记位为 1 时,才添加到result中。

用代码来完善一下:

public static ArrayList<Integer> repeat(int[] array) {ArrayList<Integer> result = new ArrayList<>();int[] exists = new int[11];for (int i = 0; i < array.length; i++) {int value = array[i];// 只有当前位置已经为1,标示重复,并且输出,>1情况则不输出了if (exists[value] == 1) {result.add(value);}// 用exists标示记录exists[value]++;}return result;
}

很明显,从代码可以看出,一次循环就能完成题目要求,所以时间复杂度为O(N)

同样,看看它们效率的差距:

暴力查找标记查找
时间复杂度O(N^2)O(N)
数组 100 个1W100
数组 1W 个1 亿1W
数组 100W 个1W 亿100W

随着数组数量增加,性能差距越来也明显。

数组

计算机内存管理

计算机的程序都是运行在内存中。

内存的工作原理

我们可以简单的将内存当做寄存柜。假设我们去游泳,需要将东西先寄存起来,寄存处有几个大箱子,每个箱子都有很多抽屉。

每个抽屉里可以放一样东西,如果你有 4 样东西,就需要放在 4 个抽屉里。 每个抽屉都有自己的编号,方便你查找和记录。 接下来,你就去游泳了,当你结束的时候,再将 4 个抽屉里面的东西带走。

这其实就是计算机内存的工作原理。

每个抽屉都可以当做内存的一个存储单元,而抽屉编号可以当做每个存储单元的内存地址。

为什么连续两个方格内存地址差 8?

因为我们现在电脑基本是 64 位,64 位表示内存存储最小使用单位64个bit,也就是8个byte

数组 - 存储和读取

为什么数组的索引从 0 开始?

数组是计算机中最基础的数据结构,每个编程语言中基本都有数组。

数组存储

要回答上面这个问题,首先得明白数组到底是怎么在内存中存储的。

数组(Array)是一种线性表数据结构。它用一组连续的内存空间,来存储一组具有相同类型的数据。

  • 线性数据结构:表示数组中数据都是按前后这种线性顺序排列的。 
  • 相同数据类型:数组中的每个值的数据类型都相同。

连续内存空间

举个生活中的例子,现在有 4 个人去电影院看电影,希望坐靠前的位置并挨着坐在一排

那如果变成 5 个人了呢?那就选择第三排。

如果把每个座位当做一块内存空间,那边连续位置就相当于数组的连续内存空间

数组读取

理解了数组的内存存储,接下来看看如何获取数组某个索引下的值?

每个数组都有对应的内存地址,将其称作为数组开始地址 --- start_address

数组在定义的时候已经确定每个数组元素的数据类型,也就是知道每个元素需要的内存空间大小,称为 --- item_size

那么数组里每个元素内存地址s都能被计算出来:

// 第一个元素地址
start_address// 第二个元素地址
start_address + item_size * 1// 第三个元素地址
start_address + item_size * 2

第 N 个元素地址公式如下:

// 第N个元素地址
start_address + item_size * (N - 1)

从这个公式可以得到两点信息:

  • 数组的索引访问的时间复杂度是O(1)
  • 内存地址的计算规则决定开始地址为start_address + item_size * 0,在计算机中为了方便位置的计算,所以数组索引从 0 开始。

补充:获取数组长度

public class ArrayLengthExample {public static void main(String[] args) {int[] array = {1, 2, 3, 4, 5};int length = array.length;System.out.println("数组的长度为: " + length);}}

数组获取索引值

   public class ArrayIndexAccess {public static void main(String[] args) {int[] array = {10, 20, 30, 40};int index = 2;int element = array[index];System.out.println("索引 " + index + " 处的元素是: " + element);}}

数组 - 插入和删除

数组插入

往数组里面插入一个元素的速度,取决于你需要把它插入到哪个位置上。

假设为周末的事情做规划,暂定规划如下

eat breakfast  // 吃早饭
shopping       // 购物
have lunch     // 吃午饭
have dinner    // 吃晚饭

如果把安排设置一个类,预留明天 20 个任务,那么代码如下:

public class Schedule {String[] array = new String[20];private int size = 0;// 内置4个安排public Schedule(){array[0] = "eat breakfast";array[1] = "shopping";array[2] = "have lunch";array[3] = "have dinner";this.size = 4;}public void print(){for(int i = 0; i < this.size; i++){System.out.print(this.array[i] + " ");}}public static void main(String[] args) {Schedule schedule = new Schedule();schedule.print();}
}

代码中内置了明天 4 个安排任务,并且设置任务数量size为 4。

尾部插入

如果现在突然想在吃完饭之后去看场电影(watch movie):

public class Schedule {String[] array = new String[20];private int size = 0;// 内置4个安排public Schedule(){array[0] = "eat breakfast";array[1] = "shopping";array[2] = "have lunch";array[3] = "have dinner";this.size = 4;}public void add(String task){this.array[this.size] = task;this.size++;}public void print(){for(int i = 0; i < this.size; i++){System.out.print(this.array[i] + " ");}}public static void main(String[] args) {Schedule schedule = new Schedule();schedule.add("watch movie");schedule.print();}
}

在尾部插入元素很容易计算,尾部插入元素内存地址为

start_address + item_size * n  // n 为当前数组的个数
中间插入

如果希望在下午吃了午饭后喝一个下午茶(afternoon tea),那应该怎么处理呢?

我们知道,需要在数组中间部分插入数据,但是为了保证数组的顺序和连续的内存空间,插入地方后面部分数据,需要往后依次移动,如图所示:

一共需要几步呢?

  1. 移动watch movie
  2. 移动have dinner
  3. 插入afternoon tea

如果插入在开始地方,则需要 N 步。

因此平均步数为 (1 + 2 + 3 + ... + N) / N 最终时间复杂度为O(N),也就是数组的插入时间复杂度为线性时间复杂度。

可以新增一个insert3Position在索引位置为 3 的地方,插入一项任务:

// 第三个位置插入
public void insert3Position(String task) {// 索引值为3的地方int index = 3;// 第一步:从右侧开始依次右移for (int i = this.size - 1; i >= index; i--) {this.array[i + 1] = this.array[i];}// 第二步:插入元素this.array[index] = task;// 调整sizethis.size++;
}
空间不够

上面我们讨论都是正常情况,如果空间不够了呢?我们再次回顾下电影院的例子:

当我们有 4 个朋友的时候,当前位置是满足要求的。如果突然多来了一个朋友,位置不够了,这个时候需要重新找寻满足 5 个人位置的空间。

这和数组空间不够的原理是一样的,需要通过 3 步完成:

如上图所示,我们一起移动到下一排:

  1. 开辟新的内存空间
  2. 复制原来的数组到新的内存空间
  3. 再进行插入操作

如果所有的内存空间都不足以存储到数组,这个时候系统就会抛出异常 --- Out Of Memory 内存不够。

举例:

public class YKDArrayList {// 此处需要声明一个数组,作为底层存储int[] array = new int[20];int size = 0;public YKDArrayList() {}// 获取数组的长度public int size() {return this.size;}// 数组获取某个索引值public int get(int index) {return this.array[index];}// 添加元素在末尾 public void add(int element) {//相当于调用传入this.sizethis.add(this.size, element);}// 添加元素在中间public void add(int index, int element) {if (index < 0 || index > this.size) {return;}// 支持扩容this.judgeMemory(this.size + 1);// 元素依次右移for (int i = this.size - 1; i >= index; i--) {this.array[i + 1] = this.array[i];}// 插入元素this.array[index] = element;// 调整sizethis.size++;}// 删除元素public void remove(int index) {if (index < 0 || index > this.size - 1) {return;}// 元素依次左移for (int i = index; i < this.size - 1; i++) {this.array[i] = this.array[i + 1];}// 删除最后一个元素this.array[this.size - 1] = 0;// 调整sizethis.size--;}private void judgeMemory(int size) {// 如果内存不满足,则需要扩容if (size > this.array.length) {// 申明两倍空间int[] newArray = new int[this.array.length * 2];// 拷贝操作this.copy(this.array, newArray);// 新数组赋值给原数组this.array = newArray;}}// 拷贝操作private void copy(int[] source, int[] aim) {for (int i = 0; i < source.length; i++) {aim[i] = source[i];}}public static void main(String[] args) {YKDArrayList ykdArrayList = new YKDArrayList();ykdArrayList.add(1);ykdArrayList.add(2);ykdArrayList.add(3);ykdArrayList.add(4);ykdArrayList.add(0, 5);ykdArrayList.remove(3);}
}

数组删除

数组的删除操作和插入操作刚好相反

如果是删除尾部元素,则直接删除;

如果是删除中间元素通过两步执行:

  1. 删除中间元素
  2. 元素右侧其他元素依次左移

因此我们可以得知数组删除的时间复杂度也为O(N)

总结

数组优势:数组的查询数据特别快,时间复杂度是 O(1)

数组劣势:数组的插入和删除比较慢,时间复杂度是 O(N) 数组需要连续内存空间,并且会申请额外的内存空间便于其扩展,这种数据结构对内存要求比较高,利用率低。

因为这些优势和劣势,我们在高频查询,低频插入和删除的情况下,可以选择数组作为底层数据结构。同时我们知道 Java 中的ArrayList,实际上在底层就是利用数组实现的。

入门排序算法

冒泡排序

一个数组变成一个有序数组需要排序算法。

冒泡排序

冒泡排序是最基础,最经典的排序算法。

如果水里有很多气泡,这些气泡都会慢慢升起,冒出水面。 并且越大的气泡,升起的速度越快,这就是冒泡的原理:对于一个数组,每一次循环让最大的元素冒泡到最后面

核心规则有 4 点:

  1. 指向数组中两个相邻的元素(最开始是数组的头两个元素),并且比较他们的大小
  2. 如果前者比后者大,则交换他们的位置
  3. 如果后者比前者大,则不交换
  4. 然后依次后移,每次循环将最大元素移动最后一个位置。

实战案例

假设有一个数组为9, 2, 4, 7, 5, 3,希望将其变成自小而大的排列。

代码实现

// 冒泡排序
public static void bubbleSort(int[] array) {// 1. 每次循环,都能冒泡出剩余元素中最大的元素,因此需要循环 array.length 次for (int i = 0; i < array.length; i++) {// 2. 每次遍历,只需要遍历 0 到 array.length - i - 1中元素,因此之后的元素都已经是最大的了for (int j = 0; j < array.length - i - 1; j++) {//3. 交换元素if (array[j] > array[j + 1]) {//倒序把>改为<即可int temp = array[j + 1];array[j + 1] = array[j];array[j] = temp;}}}
}
  • 如果要将整个数组都排好序,需要冒泡数组个数次。
  • 特别注意结束条件,array.length - i - 1。第一次遍历需要到数组结束为止,这时候已经得到最大元素9,并且放在数组末尾。第二次只需要遍历到倒数第二个元素即可。
  • 交换数组中的两个元素使用擂台法:
  1. 用临时变量存储第一个变量值
  2. 将第二个变量赋值给第一个变量
  3. 将临时变量赋值给第二个变量

最终看到排序以后的结果,从代码中可以看出比较的次数为:

N + (N - 1) + ... + 2 + 1 = (N^2 + N) / 2

最坏的情况下每次比较都需要交换,交换的次数也为(N^2 + N) / 2 所以整体的步数介于(N^2 + N) / 2(N^2 + N)之间。用大 O 记法,最终的结果为O(N^2)

选择排序

重点在选择,每次在剩余数组中,选择最大或者最小的一个,放在数组的一端。

import java.util.Arrays;
public class Sort {// 选择排序public static void selectSort(int[] array) {// 遍历N次,每次选择数组剩余元素中最大的元素for (int i = 0; i < array.length; i++) {// 暂时把第一个元素当做最大元素,并且记录最大元素的索引int maxIndex = 0;int max = array[0];// 注意 j 从 索引1 开始,到 array.length - i 截止for (int j = 1; j < array.length - i; j++) {// 如果元素大于当前最大值,则替换 max,maxIndexif (array[j] > max) {max = array[j];maxIndex = j;}}// 交换最大值元素 和 数组末尾元素int temp = array[maxIndex];array[maxIndex] = array[array.length - i - 1];array[array.length - i - 1] = temp;}}public static void main(String[] args) {int[] array = {9, 2, 4, 7, 5, 3};// Arrays.toString 可以方便打印数组内容System.out.println(Arrays.toString(array));selectSort(array);System.out.println(Arrays.toString(array));}
}

核心规则有 4 点:

  1. 利用两个变量,一个存储当前最大值,一个存储当前最大值所在的索引
  2. 依次比较后面的元素,如果发现比当前最大值大,则更新最大值,并且更新最大值所在的索引。
  3. 直到遍历结束,将最大值放在数组的最右边,也就是交换最右边元素和当前最大值元素
  4. 重复上面的步骤

时间复杂度

时间复杂度和冒泡排序一样,执行次数如下

N + (N - 1) + ... + 2 + 1

最终的结果为O(N^2)

冒泡排序 vs 选择排序

虽然两个排序算法的时间复杂度都为O(N^2),但很明显能感受到选择排序更快一点点。因为冒泡需要频繁的交换相邻两个元素,而选择排序每次遍历只需要交换一次,所以选择排序真实情况速度比冒泡排序快一倍

插入排序

重点在插入,每次抽离一个元素当做临时元素,依次比较和移动之后的其他元素,最终将这个临时元素插入对应的位置。

import java.util.Arrays;
public class Sort {// 插入排序public static void insertSort(int[] array) {// 从倒数第二位开始,遍历到底0位,遍历 N-1 次for (int i = array.length - 2; i >= 0; i--) {// 存储当前抽离的元素int temp = array[i];// 从抽离元素的右侧开始遍历int j = i + 1;while (j <= array.length - 1) {// 如果某个元素,小于临时元素,则这个元素左移if (array[j] < temp) {array[j - 1] = array[j];} else {// 如果大于,则直接将临时元素插入,然后退出循环。array[j - 1] = temp;break;}j++;}// 处理到达尾部的情况if (j == array.length) {array[j - 1] = temp;}}}public static void main(String[] args) {int[] array = {9, 2, 4, 7, 5, 3};// Arrays.toString 可以方便打印数组内容System.out.println(Arrays.toString(array));insertSort(array);System.out.println(Arrays.toString(array));}
}

核心规则有 4 点:

  1. 在第一轮,抽离数组末尾倒数第二个元素,作为临时元素。
  2. 用临时元素与数组后面的元素进行对比:如果 后面的元素 值小于临时元素,则 后面的元素 左移。
  3. 如果后面的元素大于临时元素,或者已经移动到数组末尾,则将临时元素插入当前的空隙中。
  4. 重复上面步骤,完成排序。

此处的核心逻辑已经不是选择最大元素了,而是通过每次迭代,将部分元素排好序,以达到最终全部排序的效果。

时间复杂度

最好的情况

如果所有的数组元素本身就是升序排列,每次迭代不需要进行移动,所以时间复杂度是 O(N)

最坏的情况

如果所以的数组元素都是按降序排列,每次迭代每次比较都需要移动,在这种情况下。比较的步数为 O((N^2 - N) / 2) ,移动的步数为 O((N^2 - N) / 2) 。因此最差的情况为 O(N^2 - N) ,忽略常数为 O(N^2) 。

冒泡排序 vs 选择排序 vs 插入排序

冒泡排序相比较而言肯定是较差的。 选择排序和插入排序得分情况而定,如果原始数组本身有很多元素按希望的顺序排好序,则选择插入排序(如上面最好极限情况,接近 O(N)),否则选择选择排序

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.xdnf.cn/news/9893.html

如若内容造成侵权/违法违规/事实不符,请联系一条长河网进行投诉反馈,一经查实,立即删除!

相关文章

【Linux第八课-进程间通信】管道、共享内存、消息队列、信号量、信号、可重入函数、volatile

目录 进程间通信为什么&#xff1f;是什么&#xff1f;怎么办&#xff1f;一般规律具体做法 匿名管道原理代码 命名管道原理代码 system V共享内存消息队列信号量信号量的接口 信号概念为什么&#xff1f;怎么办&#xff1f;准备信号的产生信号的保存概念三张表匹配的操作和系统…

串的模式匹配

子串的定位操作通常称为串的模式匹配&#xff0c;它求的是子串(常称模式串)在主串中的位置。 子串——主串的一部分&#xff0c;一定存在 模式串——不一定能在主串中找到 朴素模式匹配 将主串中所有长度为m的子串&#xff08;共有n-m1个&#xff09;依次与模式串对比&…

继承的学习

1.继承 继承权限在类外&#xff0c;访问权限在类内部 1.1继承的概念 继承是面向对象程序设计使代码可以复用的重要手段&#xff08;解决类层次的复用问题&#xff09; 派生类&#xff1a;类特性的基础上进行扩展&#xff0c;增加方法&#xff08;成员函数&#xff09;和属性…

YOLOPv2论文翻译

YOLOPv2: Better, Faster, Stronger for Panoptic Driving Perception 摘要 在过去的十年中&#xff0c;多任务学习方法在解决全景驾驶感知问题方面取得了令人鼓舞的成果&#xff0c;既提供了高精度又具备高效能的性能。在设计用于实时实际自动驾驶系统的网络时&#xff0c;这…

跳表原理-课堂笔记

课程地址 跳表是一种基于随机化的有序数据结构&#xff0c;它提出是为了赋予有序单链表以 O(logn) 的快速查找和插入的能力 创建 首先在头部创建一个 sentinel 节点&#xff0c;然后在 L1 层采用“抛硬币”的方式来决定 L0 层的指针是否增长到 L1 层 例如上图中&#xff0c;L…

贪心day04(买卖股票的最佳时机)

1.买卖股票的最佳时机 题目链接&#xff1a;. - 力扣&#xff08;LeetCode&#xff09; 思路&#xff1a;我们其实只需遍历一篇就可以解决这个问题。首先我们定义一个min为无穷大值&#xff0c;再遍历只要有数字比min跟小我们就更改min的值就好&#xff0c;此时我们只需要找出…

【Python爬虫实战】深入解锁 DrissionPage:ChromiumPage 自动化网页操作指南

&#x1f308;个人主页&#xff1a;易辰君-CSDN博客 &#x1f525; 系列专栏&#xff1a;https://blog.csdn.net/2401_86688088/category_12797772.html ​ 目录 前言 一、ChromiumPage基础操作 &#xff08;一&#xff09;初始化Drission 和 ChromiumPage 对象 &#xff0…

VS Code 插件 MySQL Shell for VS Code

https://marketplace.visualstudio.com/items?itemNameOracle.mysql-shell-for-vs-code

稳压二极管详解

目录 1. 工作原理 2. 稳压二极管的伏安特性曲线 3. 正向特性&#xff1a; 4. 反向特性 5. 稳定电压&#xff08;Vz&#xff09; 6. 动态电阻&#xff08;rz&#xff09; 7.最大耗散功率&#xff08;PzM&#xff09; 8. 最大稳定工作电流&#xff08;IzMAX&#xff09;和…

Springboot 一个西餐主题网站-计算机设计毕业源码73020

目录 摘要 1 绪论 1.1 选题背景与意义 1.2国内外研究现状 1.3论文结构与章节安排 2系统分析 2.1 可行性分析 2.2 系统流程分析 2.2.1 数据流程 2.2.2 业务流程 2.3 系统功能分析 2.3.1 功能性分析 2.3.2 非功能性分析 2.4 系统用例分析 2.5本章小结 3 系统总体设…

JS渗透(安全)

JS逆向 基本了解 作用域&#xff1a; 相关数据值 调用堆栈&#xff1a; 由下到上就是代码的执行顺序 常见分析调试流程&#xff1a; 1、代码全局搜索 2、文件流程断点 3、代码标签断点 4、XHR提交断点 某通js逆向结合burp插件jsEncrypter 申通快递会员中心-登录 查看登录包…

世界技能竞赛大数据应用开发环境1:1还原

关注我&#xff0c;私信我获得集群环境 集群情况 模块A搭建环境&#xff0c;在容器中搭建大数据平台 Hadoop HA环境 Pc机&#xff0c;安装安装比赛需要软件 模块B中使用idea快速开发完成数据处理 模块E包含了接口数据&#xff0c;使用vs code快速搭建vue数据可视化

【c++丨STL】vector模拟实现

&#x1f31f;&#x1f31f;作者主页&#xff1a;ephemerals__ &#x1f31f;&#x1f31f;所属专栏&#xff1a;C、STL 目录 前言 一、vector底层刨析 二、模拟实现 1. 属性、迭代器以及函数声明 2. 功能实现 交换两个容器的内容 构造函数 拷贝构造 赋值重载 析构…

指针的运用

接下来我将会用的话&#xff0c;讲解我对指针运用仅有的印象 1.解引用 int a23; int*p&a; *p666; 而*p666&#xff1b;&#xff0c;便是解引用操作&#xff0c;跟简单地说*p便是解引用&#xff0c;它的意思是&#xff0c;对p中所储存的地址所在位置的内容进行操作&#xf…

三周精通FastAPI:38 针对不同的编程语言来生成客户端

官方文档&#xff1a;https://fastapi.tiangolo.com/zh/advanced/generate-clients/ 生成客户端 因为 FastAPI 是基于OpenAPI规范的&#xff0c;自然您可以使用许多相匹配的工具&#xff0c;包括自动生成API文档 (由 Swagger UI 提供)。 一个不太明显而又特别的优势是&#…

广告联盟有哪些

随着互联网的发展&#xff0c;越来越多的人开始投身于网站建设和运营。对于站长来说&#xff0c;如何在提供优质内容的同时获取收益是一个重要的问题。广告联盟作为一种常见的盈利模式&#xff0c;受到了广大站长的青睐。本文将介绍5个适合国内站长的广告联盟平台&#xff0c;帮…

兵马未动,粮草先行-InnoDB统计数据是如何收集的

我们前面介绍查询成本的时候经常用到一些统计数据&#xff0c;比如通过SHOW TABLE STATUS可以看到关于表的统计数据&#xff0c;通过SHOW INDEX可以看到关于索引的统计数据&#xff0c;那么这些统计数据是怎么来的呢&#xff1f;它们是以什么方式收集的呢&#xff1f;本章将聚焦…

【Promise】JS 异步之宏队列与微队列

文章目录 1 原理图2 说明3 相关面试题3.1 面试题13.2 面试题23.3 面试题33.4 面试题4 1 原理图 2 说明 JS 中用来存储待执行回调函数的队列包含 2 个不同特定的队列&#xff1a;宏队列和微队列。宏队列&#xff1a;用来保存待执行的宏任务(回调)&#xff0c;比如&#xff1a;定…

基础概念理解

一&#xff0c;数据结构分类 连续结构&#xff0c;跳转结构。 二&#xff0c;对变量的理解 在 C 语言中&#xff0c;变量是用于存储数据的抽象符号。变量本质上是一块内存区域的标识符&#xff08;即它代表内存中的某一块区域&#xff09;&#xff0c;用来存储数据&#xff…

C 学习(4)

return 0; 前提&#xff1a;C 语言规定&#xff0c;main()是程序的入口函数&#xff0c;即所有的程序一定要包含一个main()函数。程序总是从这个函数开始执行&#xff0c;如果没有该函数&#xff0c;程序就无法启动。其他函数都是通过它引入程序的。 main()的写法&#xff0c…