一、数据结构和算法概述
数据结构包括:线性结构和非线性结构。
1线性结构
- 线性结构作为最常用的数据结构,其特点是数据元素之间存在一对一的线性关系。
一对一e.g. a[0] = 3; - 线性结构有两种不同的存储结构,即顺序存储结构(数组)和链式存储结构(链表)。
顺序存储的线性表称为顺序表,顺序表中的存储元素是连续的 - 链式存储的线性表称为链表,链表中的存储元素不一定是连续的,元素节点中存放数据元素以及相邻元素的地址信息(链表可以充分利用碎片内存)
- 线性结构常见的有:数组、队列、链表和栈,后面我们会详细讲解.
2非线性结构
非线性结构包括:二维数组,多维数组,广义表,树结构,图结构
1、稀疏数组
稀疏数组的基本介绍
当一个数组中大部分元素为0,或者为同一个值的数组时,可以使用稀疏数组来保存该数组。
稀疏数组的处理方法是:
- 记录数组一共有几行几列,有多少个不同的值
- 把具有不同值的元素的行列及值记录在一个小规模的数组中,从而缩小程序的规模
从7x6=42变成了3x9=27的数组
题:把棋盘用稀疏数组保存下来
代码演示
写入磁盘(fw):
读取文件(fis和fr)
2. 队列
介绍:
- 队列是一个有序列表,可以用数组或是链表来实现。
- 遵循先入先出的原则。即:先存入队列的数据,要先取出。后存入的要后取出
- 示意图:(使用数组模拟队列示意图)
数组模拟队列思路:
- 队列本身是有序列表,若使用数组的结构来存储队列的数据,则队列数组的声明如下图, 其中 maxSize 是该队列的最大容量。
- 因为队列的输出、输入是分别从前后端来处理,因此需要两个变量 front 及 rear 分别记录队列前后端的下标,front 会随着数据输出而改变,而 rear则是随着数据输入而改变,
addQueue
当我们将数据存入队列时称为”addQueue”,addQueue 的处理需要有两个步骤:思路分析
- 加入队列时,需要将尾指针往后移:先判断 front == rear (队列是否为空,是否满↓)-> rear+1 ,
- 若尾指针 rear 小于队列的最大下标 maxSize-1,则将数据存入 rear 所指的数组元素中,否则无法存入数据。
rear == maxSize -1(队列满)
加入队列和删除代码:
!注意!
因为先进先出的特点,不管是增加值到队列还是删除,front和rear都是先++,再增/删。
public class ArrayQueque{
PSVM{
}
}
class ArrayQueue{
priavte int front;
priavte int rear;
priavte int maxSize;
priavte int[] arr;
public ArrayQueue(int arrMaxSize){
maxSize = arrMaxSize;
arr = new int[maxSize];
front = -1;
rear = -1;
}
//判断队列是否为空
public boolean isEmpety(){
return rear == front;
}
//判断队列是否满
public boolean isFull(){
return rear == (maxSize-1);
}
//取出队列的值:先进先出
public int getQueue(){
if(isEmpety())
{
throw new RuntimeException("队列空,不能取数据");// 抛出异常
}else{
front++;//先进先出,所以需要后移
return arr[Front];
}
}
//添加值到队列
public void addQueue(int value){
if(isFull())
{
Sout("队列已满");
}else{
rear++;
arr[rear] = value;
}
}
遍历队列//
public void showQueue()
{
if(isEmpety())
{
sout("队列空");
}else{
for(int i = 0; i<arr.length; i++
sout("第"+i+"个是:"+arr[i]);
}
}
}
优化:数组模拟环形队列
对前面的数组模拟队列的优化,充分利用数组. 因此将数组看做是一个环形的。(通过取模的方式来实现即可)
分析说明:
- 尾索引的下一个为头索引时表示队列满,即将队列容量空出一个作为约定,这个在做判断队列满的
时候需要注意 (rear + 1) % maxSize == front 满] - rear == front [空]
- 分析示意图
问题分析并优化 - 目前数组使用一次就不能用, 没有达到复用的效果
- 将这个数组使用算法,改进成一个环形的队列 取模:%
3. 链表
链表是有序的列表
- 链表是以节点的方式来存储,是链式存储
- 每个节点包含 data 域, next 域:指向下一个节点.
- 如图:发现链表的各个节点不一定是连续存储.
- 链表分带头节点的链表和没有头节点的链表,根据实际的需求来确定
3.1 单链表
增+查:
-
添加时不考虑顺序,直接加在链表尾部
-
添加时考虑顺序,根据排名插入到指定位置;若排名存在,则添加失败。
改:根据某个元素,找到指定节点,并修改其部分内容。
思路(1) 先找到该节点,通过遍历,(2) temp.name = newHeroNode.name ; temp.nickname= newHeroNode.nickname
删:
**我的题解 - LeetCode 删除单链表的值:
https://leetcode-cn.com/problems/shan-chu-lian-biao-de-jie-dian-lcof/solution/shan-chu-lian-biao-de-zhi-by-duo-bi-e-fvvx/**
面试题:
-
求单链表中有效节点的个数(如果是带头结点的链表,需求不统计头节点)
- 判断链表是否为空
- 定义辅助变流len
- 遍历时不包括head while len++
-
查找单链表中的倒数第 k 个结点 【新浪面试题】
**我的题解 - 返回倒数第k个结点到最后的链表 (剑指offer 第22题):
https://leetcode-cn.com/problems/lian-biao-zhong-dao-shu-di-kge-jie-dian-lcof/solution/fan-hui-dao-shu-di-kge-jie-dian-dao-zui-pyh5b/**
- 单链表的反转【腾讯面试题,有点难度】
1 定义一个结点 reverseHead() = new ListNode()
2 从头到尾遍历原来的链表,每遍历一个节点,就将其取出,放入新的链表reverseHead的最前端
3 原来的链表的head.next = reverseHead.next
图解思路:⑪步
public static void reversetList(HeroNode head) {
//如果当前链表为空,或者只有一个节点,无需反转,直接返回
if(head.next == null || head.next.next == null) {
return ;
}
//定义一个辅助的指针(变量),帮助我们遍历原来的链表
HeroNode cur = head.next;
HeroNode next = null;// 指向当前节点[cur]的下一个节点
HeroNode reverseHead = new HeroNode(0, "", "");
//遍历原来的链表,每遍历一个节点,就将其取出,并放在新的链表 reverseHead 的最前端
//动脑筋
while(cur != null) {
next = cur.next;//先暂时保存当前节点的下一个节点,因为后面需要使用
cur.next = reverseHead.next;//将 cur 的下一个节点指向新的链表的最前端
reverseHead.next = cur; //将 cur 连接到新的链表上
cur = next;//让 cur 后移
}
//将 head.next 指向 reverseHead.next , 实现单链表的反转
head.next = reverseHead.next;
}
**我的题解 - 链表翻转 LeetCode 24:
https://leetcode-cn.com/problems/fan-zhuan-lian-biao-lcof/solution/lian-biao-fan-zhuan-by-duo-bi-e-8dgr/**
- 从尾到头打印单链表 【百度,要求方式 1:反向遍历 。 方式 2:Stack 栈】
利用栈的数据结构,将各个结点压入栈中,利用先进后出的特点实现逆序打印
3.2 双链表
管理单向链表的缺点分析:
- 单向链表,查找的方向只能是一个方向(需要一个一个遍历),而双向链表可以向前或者向后查找。
- 单向链表不能自我删除,需要找到前一个节点(辅助节点)才能删除。 ,而双向链表,则可以自我删除,所以前面我们单链表删除时节点,总是找到 temp,temp 是待删除节点的前一个节点(认真体会).
分析双向链表的格式、 如何完成遍历,添加,修改和删除的思路
格式:单链表 + pre[指向前一个节点]
分析 双向链表的遍历,添加,修改,删除的操作思路===》代码实现
-
遍历 方和 单链表一样,只是可以向前,也可以向后查找
-
添加 (默认添加到双向链表的最后)
(1) 先找到双向链表的最后的节点(即next=null)
//2.3.形成一个双向链接
(2) temp.next = newHeroNode (指向后一个节点)
(3) newHeroNode.pre = temp; (指向前一个节点) -
修改 思路和 原来的单向链表一样.
-
删除
(1) 因为是双向链表,因此,我们可以实现自我删除某个节点
(2) 直接找到要删除的这个节点,比如 temp
//3.4.形成一个双向链接
(3) temp.pre.next = temp.next
if(temp.next != null){
(4) temp.next.pre = temp.pre; (如果是最后一个结点,就不需要执行此代码)
}
3.3 约瑟夫环问题(使用单向环形链表解决)
Josephu 问题为:
设编号为 1,2,… n 的 n 个人围坐一圈,约定编号为 k(1<=k<=n)的人从 1 开始报数,数到m 的那个人出列,它的下一位又从 1 开始报数,数到 m 的那个人又出列,依次类推,直到所有人出列为止,由此产生一个出队编号的序列。
提示
用一个不带头结点的循环链表来处理 Josephu 问题:先构成一个有 n 个结点的单循环链表,然后由 k 结点起从 1 开始计数,计到 m 时,对应结点从链表中删除,然后再从被删除结点的下一个结点又从 1 开始计数,直到最后一个结点从链表中删除算法结束
约瑟夫问题-创建环形链表的思路图解:
构建和遍历
- first指针始终指向第一个节点,
- 最后一个节点的next始终指向first
- 辅助指针(current)来找最后一个节点
4.栈
栈的介绍:
栈(stack)
- 栈是一个先入后出(FILO-First In Last Out)的有序列表。
- 栈(stack)是限制线性表中元素的插入和删除只能在线性表的同一端进行的一种特殊线性表。允许插入和删除的
一端,为变化的一端,称为栈顶(Top),另一端为固定的一端,称为栈底(Bottom)。 - 根据栈的定义可知,最先放入栈中元素在栈底(入栈(push)),最后放入的元素在栈顶,而删除元素刚好相反,最后放入的元
素最先删除(出栈(pop)),最先放入的元素最后删除
栈的应用场景
- 子程序的调用:在跳往子程序前,会先将下个指令的地址存到堆栈中,直到子程序执行完后再将地址取出,以
回到原来的程序中。 - 处理递归调用:和子程序的调用类似,只是除了储存下一个指令的地址外,也将参数、区域变量等数据存入堆
栈中。 - 表达式的转换[中缀表达式转后缀表达式]与求值(实际解决)。
- 二叉树的遍历。
- 图形的深度优先(depth 一 first)搜索法。
实现栈的思路
- 使用数组来模拟栈
- 定义一个 top 来表示栈顶,初始化 为 -1
- 入栈的操作,当有数据加入到栈时, top++; stack[top] = data;
- 出栈的操作, int value = stack[top]; top–, return value
- 遍历:从栈顶开始遍历, for(int i = top; i>0; i–)
栈实现综合计算器:中缀表达式
代码实现[1. 先实现一位数的运算, 2. 扩展到多位数的运算]
前缀(波兰式)、中缀:运算符在操作数之间、后缀表达式(后波兰式)
前缀(波兰式):从右向左扫描表达式
中缀:需要判断运算符的优先级,对机算计并不方便。
后缀表达式(后波兰式):一般中缀会转为后缀,后缀最易操作
从左向右扫描表达式
中缀表达式转后缀表达式
具体步骤如下:
- 初始化两个栈:运算符栈 s1 和储存中间结果的栈 s2;
- 从左至右扫描中缀表达式;
- 遇到操作数时,将其压 s2;
- 遇到运算符时,比较其与 s1 栈顶运算符的优先级:
1.如果 s1 为空,或栈顶运算符为左括号“(”,则直接将此运算符入栈;
2.否则,若优先级比栈顶运算符的高,也将运算符压入 s1;
3.否则,将 s1 栈顶的运算符弹出并压入到 s2 中,再次转到(4-1)与 s1 中新的栈顶运算符相比较; - 遇到括号时:
(1) 如果是左括号“(”,则直接压入 s1
(2) 如果是右括号“)”,则依次弹出 s1 栈顶的运算符,并压入 s2,直到遇到左括号为止,此时将这一对括号丢弃 - 重复步骤 2 至 5,直到表达式的最右边
- 将 s1 中剩余的运算符依次弹出并压入 s2
- 依次弹出 s2 中的元素并输出,结果的逆序即为中缀表达式对应的后缀表达式
举例说明:
将中缀表达式“1+((2+3)×4)-5”转换为后缀表达式的过程如下
因此结果为 :“1 2 3 + 4 × + 5 –”
5. 递归
应用场景:迷宫问题/回溯
概念:递归就是方法自己调用自己,每次调用时传入不同的变量。
递归调用规则:
- 当程序执行到一个方法时,就会开辟一个独立的空间(栈)
-
- 每个空间的数据(局部变量),是独立的.
递归可用于解决什么样的问题:
- 各种数学问题如: 8 皇后问题 , 汉诺塔, 阶乘问题, 迷宫问题, 球和篮子的问题(google 编程大赛)
- 各种算法中也会使用到递归,比如快排,归并排序,二分查找,分治算法等.
- 将用栈解决的问题–>第归代码比较简洁
递归需要遵守的重要规则
递归需要遵守的重要规则
- 执行一个方法时,就创建一个新的受保护的独立空间(栈空间)
- 方法的局部变量是独立的,不会相互影响, 比如 n 变量
- 如果方法中使用的是引用类型变量(比如数组),就会共享该引用类型的数据.
- 递归必须向退出递归的条件逼近,否则就是无限递归,出现 StackOverflowError,死循环:)
- 当一个方法执行完毕,或者遇到 return,就会返回,遵守谁调用,就将结果返回给谁,同时当方法执行完毕或
者返回时,该方法也就执行完毕
6 .排序算法Sort Algorithm(重要 面试经常考)
定义:排序是将一组数据,根据指定的顺序进行排序的过程
排序的分类:
1. 内部排序(8种,重点,面试经常考)
把需要处理的所有数据加载入内部存储器/内存中进行排序
- 外部排序
当数据量过大,无法全部加载到内存中,需要借助外部存储进行排序
算法时间复杂度(如何衡量一个程序执行时间)
-
事后统计法
局限:
需要先运行程序,需要等;
依赖于计算机的硬件、软件等环境。 -
事前估算法
通过分析某个算法的时间复杂度来判断哪个算法更优。
时间复杂度
时间频度:一个算法花费的时间与算法中语句的执行次数成正比。一个算法中的语句执行次数称为语句频度或时间频度。
基本案例:
随着n的变大,有三个特点:
忽略常数项,忽略低次项(n的一次方),忽略系数。
所以时间复杂度主要还是看n的高次方
函数T(n)可能不相同,时间复杂度O(n)可能相同
计算时间复杂度的Steps:
- 用常数1代替运行时间中的所有加法常数 T(n)=5n²+7n+6 => T(n)=5n²+7n+1
- 修改后的运行次数函数中,只保留最高阶项 T(n)=n²+7n+1 => T(n) = 5n²
- 去除最高阶项的系数 T(n) = 5n² => T(n) = n² => O(n²)
常见的时间复杂度 8 个必知的
常见的时间复杂度
-
常数阶O(1) :没有循环等复杂结构,就算有几十万行,也是O(1)
-
对数阶O(log2n) : 常数的n次方
e.g while(i < n)
i=i*2;
2的 x 次方等于 n即可退出循环,那么 x = log2n也就是说当循环 log2n 次以后
(N=a的x次方,x=loga N) -
线性阶O(n) :一个for循环,这个for循环会执行n次,T(n)=n+1,时间复杂度为O(n)。
-
线性对数阶O(nlog2n) :for 套 对数阶 = O(n*log2n)
-
平方阶O(n²) :双层for循环
-
立方阶O(n³) :三层for循环
-
k次方阶O(nk) :嵌套了k次for循环
-
指数阶O(2ⁿ) :尽量避免
说明: 常见的算法时间复杂度由小到大依次为:Ο(1)<Ο(log2n)<Ο(n)<Ο(nlog2n)<Ο(n2)<Ο(n3)< Ο(nk) <Ο(2n) ,随着问题规模n的不断增大,上述时间复杂度不断增大,算法的执行效率越低 从图中可见,我们应该尽可能避免使用指数阶2ⁿ的算法
算法的时间复杂度
空间复杂度
基本介绍
- 类似于时间复杂度的讨论,一个算法的空间复杂度(Space Complexity)定义为该算法所耗费的存储空间,它也是问题规模n的函数。
- 空间复杂度(Space Complexity)是对一个算法在运行过程中临时占用存储空间大小的量度。有的算法需要占用的临时工作单元数与解决问题的规模n有关,它随着n的增大而增大,当n较大时,将占用较多的存储单元,例如快速排序和归并排序算法就属于这种情况
- 在做算法分析时,主要讨论的是时间复杂度。从用户使用体验上看,更看重的程序执行的速度。一些缓存产品(redis, memcache)和算法(基数排序)本质就是用空间换时间.
1. 冒泡排序Bubble Sorting
基本思想是:
通过对待 排序序列从前向后(从下标较小的元素开始),依次比较相邻元素的值,若发现逆序则交换,使值较大的元素逐渐从前移向后部,就象水底下的气泡一样逐渐向上冒。
因为排序的过程中,各元素不断接近自己的位置,如果一趟比较下 来没有进行过交换,就说明序列有序,因此要在排序过程中设置 一个标志flag判断元素是否进行过交换。从而减少不必要的比较。(这里说的优化,可以在冒泡排序写好后,在进行)
e.g. 原始数组:3,9,-1,10,20 数组大小:size=5
总共需要size-1次
第一趟排序
(1) 3, 9, -1, 10, 20 // 如果相邻的元素逆序就交换
(2) 3, -1, 9, 10, 20
(3) 3, -1, 9, 10, 20
(4) 3, -1, 9, 10, 20
第二趟排序
(1) -1, 3, 9, 10, 20 //交换
(2) -1, 3, 9, 10, 20
(3) -1, 3, 9, 10, 20
第三趟排序
(1) -1, 3, 9, 10, 20
(2) -1, 3, 9, 10, 20
第四趟排序
(1) -1, 3, 9, 10, 20
小结冒泡排序规则 for{for{}}
(1) 一共进行 数组的大小 size-1次 的大循环
(2) 每一趟排序的次数在逐渐的减少
(3) 优化:如果我们发现在某趟排序中,没有发生一次交换, 可以提前结束冒泡排序。
代码:
原数组:{3,9,-1,10,20};
原数组:{3,9,-1,10,-2};
- 优化:如果我们发现在某趟排序中,没有发生一次交换, 可以提前结束冒泡排序。
使用flag (boolean)表示是否进行果交换
优化过后,提前结束冒泡排序
计算排序时间:8w个冒泡排序是10秒
2. 选择排序
是从预排序的数据中,按指定的规则选出某一元素,再依照规定交换位置后达到排序的目的
从整个数组中找到最小值,跟arr[0]交换,(范围:arr[0] ~ arr[n-1])
第二次从数组中找到最小值,跟arr[1]交换。(范围:arr[1] ~ arr[n-1])
以此类推,总共需要size-1次,得到一个按排序码从小到大排列的有序序列。
原始的数组 : 101, 34, 119, 1
第一轮排序 :
1, 34, 119, 101
第二轮排序 :
1, 34, 119, 101
第三轮排序 :
1, 34, 101, 119
说明:
- 选择排序一共有 数组大小 - 1 轮排序
- 每1轮排序,又是一个循环, 循环的规则(代码)
2.1先假定当前这个数是最小数 arr[0]
2.2 然后和后面的每个数进行比较,如果发现有比当前数更小的数,就重新确定最小数,并得到下标
2.3 当遍历到数组的最后时,就得到本轮最小数和下标
2.4 交换 [代码中再继续说 ]
选择排序算法快于冒泡算法
public static void selectSort(int[] arr) {
//在推导的过程,我们发现了规律,因此,可以使用 for 来解决
//选择排序时间复杂度是 O(n^2)
for (int i = 0; i < arr.length - 1; i++) {
int minIndex = i;
int min = arr[i];
for (int j = i + 1; j < arr.length; j++) {
if (min > arr[j]) {
// 说明假定的最小值,并不是最小; 即前一个比后一个大,应该要换位
min = arr[j]; // 重置 min;即换位
minIndex = j; // 重置 minIndex;即换下标
//本轮的最小值已经找到了,接下来需要进行交换↓
}
}
// 将最小值放在 arr[0], 即交换
if (minIndex != i) {
arr[minIndex] = arr[i]; //把arr[i]跟上面找到的最小值的位置[minIndex]进行交换
arr[i] = min; //min是最小值,放在最前面
}
System.out.println("第"+(i+1)+"轮后~~");
System.out.println(Arrays.toString(arr));// 1, 34, 101,119
}
lintcode题2:
3. 插入排序Insertion Sorting
介绍:
插入式排序属于内部排序法,是对于欲排序的元素以插入的方式找寻该元素的适当位置,以达到排序的目的。
插入排序(Insertion Sorting)的基本思想是:
把n个待排序的元素看成为一个有序表和一个无序表,开始时有序表中只包含一个元素,无序表中包含有n-1个元素,排序过程中每次从无序表中取出第一个元素,把它的排序码依次与有序表元素的排序码进行比较,将它插入到有序表中的适当位置,使之成为新的有序表。
次数:size-1次
//插入排序
public static void insertSort(int[] arr)
{
int insertVal = 0; int insertIndex = 0
//使用 for 循环来把代码简化
for(int i = 1; i < arr.length; i++)
{
//定义待插入的数
insertVal = arr[i];
insertIndex = i - 1; // 即 arr[1]的前面这个数的下标
// 给 insertVal 找到插入的位置
// 说明
// 1. insertIndex >= 0 保证在给 insertVal 找插入位置,不越界
// 2. insertVal < arr[insertIndex] 待插入的数,还没有找到插入位置
// 3. 就需要将 arr[insertIndex] 后移
while (insertIndex >= 0 && insertVal < arr[insertIndex]) {
arr[insertIndex + 1] = arr[insertIndex];// arr[insertIndex]
insertIndex--;
}
// 当退出 while 循环时,说明插入的位置找到, insertIndex + 1
// 举例:理解不了,我们一会 debug
//这里我们判断是否需要赋值
if(insertIndex + 1 != i) {
//该放的位置就是当前的位置
arr[insertIndex + 1] = insertVal;
}
System.out.println("第"+i+"轮插入");
System.out.println(Arrays.toString(arr));
}
插入和冒泡算法话费的时间差不多
4. 希尔排序shell
基本排序的更高效的版本,也称缩小增量排序
基本思想:希尔排序是把记录按下标的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至1时,整个文件恰被分成一组,算法便终止
希尔排序法应用:
- 希尔排序时, 对有序序列在插入时采用交换法, 并测试排序速度.
- 希尔排序时, 对有序序列在插入时采用移动法, 并测试排序速度
// 使用逐步推导的方式来编写希尔排序
// 希尔排序时, 对有序序列在插入时采用交换法, // 思路(算法) ===> 代码
public static void shellSort(int[] arr) {
int temp = 0;
int count = 0;
// 根据前面的逐步分析,使用循环处理
for (int gap = arr.length / 2; gap > 0; gap /= 2) {
for (int i = gap; i < arr.length; i++) {
// 遍历各组中所有的元素(共 gap 组,每组有个元素), 步长 gap
for (int j = i - gap; j >= 0; j -= gap) {
// 如果当前元素大于加上步长后的那个元素,说明交换
if (arr[j] > arr[j + gap]) {
temp = arr[j];
arr[j] = arr[j + gap];
arr[j + gap] = temp;
}
}
}
//System.out.println("希尔排序第" + (++count) + "轮 =" + Arrays.toString(arr));
}
交换希尔排序比插入慢
优化:移动法,比交换希尔排序 和插入 快很多,十分的厉害
//对交换式的希尔排序进行优化->移位法
public static void shellSort2(int[] arr) {
// 增量 gap, 并逐步的缩小增量
for (int gap = arr.length / 2; gap > 0; gap /= 2) {
// 从第 gap 个元素,逐个对其所在的组进行直接插入排序
for (int i = gap; i < arr.length; i++) {
int j = i;
int temp = arr[j];
if (arr[j] < arr[j - gap]) {
while (j - gap >= 0 && temp < arr[j - gap]) {
//移动
arr[j] = arr[j-gap];
j -= gap;
}
//当退出 while 后,就给 temp 找到插入的位置
arr[j] = temp;
}
}
}
5. 快速排序(Quicksort)
介绍:
快速排序(Quicksort)是对冒泡排序的一种改进。
基本思想是:
通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列
步骤
1)找到中位数pivot,
2)一直找,找到左边比pivot大的;再一直找,找到右边比pivot小的。
3)进行交换;
4)递归 recursion
. 4.1 左递归
. 4.2 右递归
我的题解 - leetcode18 将数组的奇数放在数组的前半部分,偶数放在数组的后半部分:
https://leetcode-cn.com/problems/diao-zheng-shu-zu-shun-xu-shi-qi-shu-wei-yu-ou-shu-qian-mian-lcof/solution/qi-shu-fang-zai-shu-zu-de-qian-ban-bu-fe-v7mr/
我的题解 - LeetCode剑指40 k个数最小的元素
https://leetcode-cn.com/problems/zui-xiao-de-kge-shu-lcof/solution/kge-shu-zui-xiao-de-yuan-su-by-duo-bi-e-vgnx/
快速排序十分快!8w数据0~1秒
快排实例:
原数组:[-9,78,0,23,-567,70]
思路:标志数x 在x左边找比x大的数, 在x右边找比x小的数, 进行交换
- 快速排序得到:x = 0的话,0左边的78比0大;0右边的-567比0小,进行交换,得到:【-9,-567,0,23,78,70】
- 进行左递归得到:【-567,-9,0,23,78,70】
- 进行右递归得到:【-567,-9,0,23,70,78】
public class QuickSort {
public static void main(String[] args) {
}
//1. 快速排序
public static void quickSort(int[] arr,int left, int right) {
int l = left; //左下标
int r = right; //右下标
//pivot 中轴值
int pivot = arr[(left + right) / 2];
int temp = 0; //临时变量,作为交换时使用
//while 循环的目的是让比 pivot 值小放到左边
//比 pivot 值大放到右边
while( l < r) {
//在 pivot 的左边一直找,找到大于等于 pivot 值,才退出
while( arr[l] < pivot) {
l += 1;
}
//在 pivot 的右边一直找,找到小于等于 pivot 值,才退出
while(arr[r] > pivot) {
r -= 1;
}
//如果 l >= r, 说明 pivot 的左右两的值,都是按顺序的。
//已经按照左边全部是小于等于 pivot 值,右边全部是大于等于 pivot 值
if( l >= r) {
break;
}
//交换
temp = arr[l];
arr[l] = arr[r];
arr[r] = temp;
//如果交换完后,发现这个 左边的值arr[l] == pivot 值 相等 r--, 前移
if(arr[l] == pivot) {
r -= 1;
}
//如果交换完后,发现这个 arr[r] == pivot 值 相等 l++, 后移
if(arr[r] == pivot) {
l += 1;
}
}
//递归前的判断: 如果 l == r, 必须 l++, r--, 否则为出现栈溢出
if (l == r) {
l += 1;
r -= 1;
}
//2. 向左递归,即pivot左边进行排序
if(left < r) {
quickSort(arr, left, r);
}
//3. 向右递归,即pivot右边进行排序
if(l < right ) {
quickSort(arr, l, right);
}
}
6. 归类排序Merge Sort
介绍:是利用归并的思想实现的排序方法,该算法采用经典的分治算法策略
(分治法:分阶段:将问题分成一些小问题然后递归求解,治阶段:则将分的阶段得到的答案修补在一起)
归并排序时间很短!!!8w数据0~1秒
题:合并两个有序序列
e.g.
分:-
将[4,5,7,8]和[1,2,3,6]两个已经有序的子序列,合并为最终序列[1,2,3,4,5,6,7,8]
合并:
7. 基数排序
- 基数排序(radix sort)属于“分配式排序”(distribution sort),又称“桶子法”(bucket sort)或 bin sort,顾名思义,它是通过键值的各个位的值,将要排序的元素分配至某些“桶”中,达到排序的作用
- 基数排序法是属于稳定性的排序,基数排序法的是效率高的稳定性排序法
- 基数排序(Radix Sort)是桶排序的扩展
- 基数排序是 1887 年赫尔曼·何乐礼发明的。它是这样实现的:将整数按位数切割成不同的数字,然后按每个位数分别比较。
基数排序基本思想
- 将所有待比较数值统一为同样的数位长度,数位较短的数前面补零。然后,从最低位开始,依次进行一次排序。这样从最低位排序一直到最高位排序完成以后, 数列就变成一个有序序列。
- 这样说明,比较难理解,下面我们看一个图文解释,理解基数排序的步骤
基数排序十分快;8w1s,80w1s,800w1s,但是会耗费内存。
图文说明:
代码:
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Date;
public class RadixSort {
public static void main(String[] args) {
//根据前面的推导过程,我们可以得到最终的基数排序代
int arr[] = {
53, 3, 542, 748, 14,214};
radixSort(int[] arr)
}
//基数排序方法
public static void radixSort(int[] arr)
{
//1. 得到数组中最大的数的位数
int max = arr[0]; //假设第一数就是最大数
for(int i = 1; i < arr.length; i++)
{
if (arr[i] > max)
{
max = arr[i];
}
}
//得到最大数是几位数
int maxLength = (max + "").length();//比如最大数为647,变成string之后,用length()可知长度为3。
//定义一个二维数组,表示 10 个桶, 每个桶就是一个一维数组
//说明
//1. 二维数组包含 10 个一维数组
//2. 为了防止在放入数的时候,数据溢出,则每个一维数组(桶),大小定为 arr.length
//3. 名明确,基数排序是使用空间换时间的经典算法
int[][] bucket = new int[10][arr.length];
//为了记录每个桶中,实际存放了多少个数据,我们定义一个一维数组来记录各个桶的每次放入的数据个数
//可以这里理解
//比如:bucketElementCounts[0] , 记录的就是 bucket[0] 桶的放入数据个数
int[] bucketElementCounts = new int[10];
//这里我们使用循环将代码处理
for(int i = 0 , n = 1; i < maxLength; i++, n *= 10) {
//(针对每个元素的对应位进行排序处理), 第一次是个位,第二次是十位,第三次是百位.. for(int j = 0; j < arr.length; j++)
{
//取出每个元素的对应位的值
int digitOfElement = arr[j] / n % 10;
//放入到对应的桶中
bucket[digitOfElement][bucketElementCounts[digitOfElement]] = arr[j];
//bucketElementCounts[digitOfElement]=0 因为没有赋值,初始化为0;
bucketElementCounts[digitOfElement]++;
}
//按照这个桶的顺序(一维数组的下标依次取出数据,放入原来数组)
int index = 0;
//遍历每一桶,并将桶中是数据,放入到原数组
for(int k = 0; k < bucketElementCounts.length; k++)
{
//如果桶中,有数据,我们才放入到原数组
if(bucketElementCounts[k] != 0)
{
//循环该桶即第 k 个桶(即第 k 个一维数组), 放入
for(int l = 0; l < bucketElementCounts[k]; l++)
{
//取出元素放入到 arr
arr[index++] = bucket[k][l];
}
}
//第 i+1 轮处理后,需要将每个 bucketElementCounts[k] = 0 !!!!
bucketElementCounts[k] = 0;
}
System.out.println("第"+(i+1)+"轮,对个位的排序处理 arr =" + Arrays.toString(arr));
}
基数排序的说明:
- 基数排序是对传统桶排序的扩展,速度很快.
- 基数排序是经典的空间换时间的方式,占用内存很大, 当对海量数据排序时,容易造成 OutOfMemoryError 。
- 基数排序时稳定的。[注:假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,r[i]=r[j],且 r[i]在 r[j]之前,而在排序后的序列中,r[i]仍在 r[j]之前,则称这种排序算法是稳定的;否则称为不稳定的]
- 有负数的数组,我们不用基数排序来进行排序, 如果要支持负数,参考: https://code.i-harness.com/zh-CN/q/e98fa9
8. 第八个堆排序跟二叉树相关,等二叉树那再讲
七个常用排序总结与对比
相关术语:
- 稳定:相同的数在排序之后位置不会变。比如 1a = 1b, [1a,56,8,5,6,2,8,1b], 1a在1b前面,排序后,1a任然在1b前面
- 不稳定: 1a在1b前面,排序后,1a可能在1b后面
- 内排序:在内存里进行排序
- 外排序:在内存外进行排序
- 时间复杂度:一个算法执行所消耗的时间
- 空间复杂度:一个算法执行所消耗的空间/内存大小
- n:数据规模
- k:桶的个数
- in-place:不占用额外内存空间
- out-place:占用额外内存空间
n越大,基数比其他的更快
n超过十后,基数比其他线性阶的更快
7. 查找算法:常见4个
- 顺序(线性)查找
- 二分查找/折半查找
- 插值查找
- 斐波那契查找
1. 顺序(线性)查找
【顺序查找】就是最简单的用for循环找某个值,传入一个数组和标志,有提示找到 并给出下标值
e.g.
有一个数列: {1,8, 10, 89, 1000, 1234} ,判断数列中是否包含此名称
【顺序查找】 要求: 如果找到了,就提示找到,并给出下标值
public class SeqSearch {
public static void main(String[] args)
{
int arr[] = {
1, 9, 11, -1, 34, 89 };// 没有顺序的数组
int index = seqSearch(arr, -11);
if(index == -1)
{
System.out.println("没有找到到");
}
else
{
System.out.println("找到,下标为=" + index);
}
}
/**
* 这里我们实现的线性查找是找到一个满足条件的值,就返回
* @param arr
* @param value
* @return
*/
public static int seqSearch(int[] arr, int value)
{
// 线性查找是逐一比对,发现有相同值,就返回下标
for (int i = 0; i < arr.length; i++) {
if(arr[i] == value)
return i;
}
return -1;
}
}
2. 二分查找算法
二分查找:
请对一个有序数组进行二分查找 {1,8, 10, 89, 1000, 1234} ,输入一个数看看该数组是否存在此数,并且求出下标,如果没有就提示"没有这个数"。
课后思考题: {1,8, 10, 89, 1000, 1000,1234} 当一个有序数组中,有多个相同的数值时,如何将所有的数值都查找到,比如这里的 1000.
二分查找的思路分析
- 首先确定该数组的中间的下标 mid = (left + right) / 2
- 然后让需要查找的数 findVal 和 arr[mid] 比较
2.1 findVal > arr[mid] , 说明你要查找的数在mid 的右边, 因此需要递归的向右查找
2.2 findVal < arr[mid], 说明你要查找的数在mid 的左边, 因此需要递归的向左查找
2.3 findVal == arr[mid] 说明找到,就返回 - 结束递归.
- 找到就结束递归
- 递归完整个数组,仍然没有找到findVal ,也需要结束递归 当 left > right 就需要退出
public class BinarySearch {
public static void main(String[] args) {
int arr[] = {
1, 8, 10, 89,1000,1000, 1234 };
// int resIndex = binarySearch(arr, 0, arr.length - 1, 1000);
// System.out.println("resIndex=" + resIndex);
List<Integer> resIndexList = binarySearch2(arr, 0, arr.length - 1, 1000);
System.out.println("resIndexList=" + resIndexList);
}
// 二分查找算法
/**
* @param arr数组
* @param left左边的索引
* @param right右边的索引
* @param findVal要查找的值
* @return 如果找到就返回下标,如果没有找到,就返回 -1
*/
public static int binarySearch(int[] arr, int left, int right, int findVal) {
// 当 left > right 时,说明递归整个数组,但是没有找到
if (left > right) {
return -1;
}
int mid = (left + right) / 2;
int midVal = arr[mid];
if (findVal > midVal) {
// 向 右递归
return binarySearch(arr, mid + 1, right, findVal);
} else if (findVal < midVal) {
// 向左递归
return binarySearch(arr, left, mid - 1, findVal);
} else {
//找到了
return mid;
}
}
若要找的数,有多个相同的数值时,可将所有相同的值放入ArrayList
/*
* 课后思考题: {1,8, 10, 89, 1000, 1000,1234} 当一个有序数组中,
* 有多个相同的数值时,如何将所有的数值都查找到,比如这里的 1000
*
* 思路分析
* 1. 在找到 mid 索引值,不要马上返回
* 2. 向 mid 索引值的左边扫描,将所有满足 1000, 的元素的下标,加入到集合 ArrayList
* 3. 向 mid 索引值的右边扫描,将所有满足 1000, 的元素的下标,加入到集合 ArrayList
* 4. 将 Arraylist 返回
*/
public static List<Integer> binarySearch2(int[] arr, int left, int right, int findVal) {
// 当 left > right 时,说明递归整个数组,但是没有找到
if (left > right) {
return new ArrayList<Integer>