一、基本概念
程序=数据结构+算法
一、数据结构
1、基本概念
数据结构表示数据在计算机中的存储和组织形式,主要描述数据元素之间的位置关系等。选择适当的数据结构可以提高计算机程序的运行效率(时间复杂度)和存储效率(空间复杂度)
2、三种层次
1、 逻辑结构–抽象层(主要描述的是数据元素之间的逻辑关系)
结构 | 描述 |
---|---|
集合结构(集) | 所有的元素都属于一个总体,除了同属于一个集合外没有其他关系。集合结构不强调元素之间的任何关联性 |
线性结构(表) | 数据元素之间具有一对一的前后关系。结构中必须存在唯一的首元素和唯一的尾元素。例如一维数组、队列、栈 |
树形结构(树) | 数据元素之间一对多的关系 |
网状结构(图) | 图状结构或网状结构。结构中的数据元素之间存在多对多的关系 |
2、 物理结构–存储层(主要描述的是数据元素之间的位置关系)
结构 | 描述 | 优点 | 缺点 |
---|---|---|---|
顺序结构 | 顺序结构就是使用一组连续的存储单元依次存储逻辑上相邻的各个元素 | 只需要申请存放数据本身的内存空间即可,支持下标访问,也可以实现随机访问 | 必须静态分配连续空间,内存空间的利用率比较低。插入或者删除可能需要移动大量元素,效率比较低 |
链式结构 | 链式存储结构不使用连续的存储空间存放结构的元素,而是为每一个元素构造一个节点。节点中除了存放数据本身以外,还需要存放指向下一个节点的指针 | 不采用连续的存储空间导致内存空间利用率比较高,克服顺序存储结构中预知元素个数的缺点,插入或者删除元素时,不需要移动大量的元素 | 需要额外的空间来表达数据之间的逻辑关系,不支持下标访问和随机访问 |
索引结构 | 除建立存储节点信息外,还建立附加的索引表来标节点的地址。索引表由若干索引项组成 | 使用节点的索引号来确定节点存储地址,检索速度快 | 增加了附加的索引表,会占用较多的存储空间 |
散列结构 | 由节点的关键码值决定节点的存储地址。散列技术除了可以用于查找外,还可以用于存储 | 散列是数组存储方式的一种发展,采用存储数组中内容的部分元素作为映射函数的输入,映射函数的输出就是存储数据的位置,相比数组,散列的数据访问速度要高于数组 | 不支持排序,一般比用线性表存储需要更多的空间,并且记录的关键字不能重复 |
3、 运算结构–实现层(主要描述的是如何实现数据结构)
分配资源,建立结构,释放资源
插入和删除
获取和遍历
修改和排序
二、数据结构
1、常用数据结构
2、数据结构比较
数据结构 | 优点 | 缺点 |
---|---|---|
无序数组 | 插入快 | 查找、删除慢,大小固定,只能存储单一元素 |
有序数组 | 比无序数组查询快,可以二分查找 | 插入、删除慢,大小固定,只能存储单一元素 |
栈 | 提供后进先出的存取方式 | 存取其他项很慢 |
队列 | 提供先进先出的存取方式 | 存取其他项很慢 |
链表 | 插入快,删除快 | 查找慢 |
散列表 | 如果关键字已知则存取极快 | 删除慢,如果不知道关键字则存取慢,对存储空间使用不充分 |
堆 | 插入、删除快,对最大数据项存取快 | 对其他数据项存取慢 |
图 | 对现实世界建模 | 有些算法慢且复杂 |
二叉搜索树 | 查找、插入、删除都快 | 删除算法复杂 |
平衡二叉树 | 查找、插入、删除都快 | 插入删除涉及左右旋转,有一定损耗,删除算法复杂 |
红黑树 | 查找、删除、插入都快,树总是平衡的 | 插入删除涉及左右旋转,有一定损耗,算法复杂 |
2-3-4数 | 查找、删除、插入都快,树总是平衡的。类似的树对磁盘存储有效 | 算法复杂 |
O符号
O在算法当中表述的是时间复杂度,它在分析算法复杂性的方面非常有用。常见的有:
O(1):最低的复杂度,无论数据量大小,耗时都不变,都可以在一次计算后获得。哈希算法就是典型的O(1)
O(n):线性,n表示数据的量,当量增大,耗时也增大,常见有遍历算法
O(n²):平方,表示耗时是n的平方倍,当看到循环嵌循环的时候,基本上这个算法就是平方级的,如:冒泡排序等
O(log n):对数,通常ax=n,那么数x叫做以a为底n的对数,也就是x=logan,这里是a通常是2,如:数量增大8倍,耗时只增加了3倍,二分查找就是对数级的算法,每次剔除一半
O(n log n):线性对数,就是n乘以log n,按照上面说的数据增大8倍,耗时就是8*3=24倍,归并排序就是线性对数级的算法
3、数据结构详述
1、Array
- 在Java中,数组是用来存放同一种数据类型的集合,注意只能存放同一种数据类型。
- 大小固定,不能动态扩展(初始化给大了,浪费;给小了,不够用)。
- 查找快,时间复杂度O(1),因为数组是一片连续的内存空间,数据的获取都是先计算内存地址,然后再加载对应内存地址的数据。数组的引用存储数组的第一个元素的内存地址,后面的元素通过第一个元素的地址然后加上偏移地址可以快速的定位到任意元素的内存地址,然后取出数据。
- 插入慢,时间复杂度O(n),默认插入数组的尾部。如果需要插入某个指定的位置, 需要移动元素。
2、Stack
- 栈(stack)又称为堆栈或堆叠,栈作为一种数据结构,它按照先进后出的原则存储数据,先进入的数据被压入栈底,最后的数据在栈java中Stack是Vector的一个子类,只定义了默认构造函数,用来创建一个空栈。
- 栈是元素的集合,其包含了两个基本操作:push操作可以用于将元素压入栈,pop 操作可以将栈顶元素移除。 遵循后入先出(LIFO)原则。
3、Queue
- 队列是元素的集合,其包含了两个基本操作:enqueue 操作可以用于将元素插入到队列中,而 dequeue 操作则是将元素从队列中移除。
遵循先入先出原则 (FIFO)。
4、Linked List
- 链表即是由节点(Node)组成的线性集合,每个节点可以利用指针指向其他节点。它是一种包含了多个节点的、能够用于表示序列的数据结构。
- 单向链表: 链表中的节点仅指向下一个节点,并且最后一个节点指向空。
- 双向链表: 其中每个节点具有两个指针 p、n,使得 p指向先前节点并且 n 指向下一个节点;最后一个节点的 n 指针指向 null。
- 循环链表:每个节点指向下一个节点并且最后一个节点指向第一个节点的链表。
- 插入快,时间复杂度O(1),基本上只需要替换2次就可以实现单链表元素的插入操作。
- 查找慢,时间复杂度O(n)。每次都需要从头结点开始遍历。
5、Binary Tree
- 二叉树(由一个根结点和两棵互不相交的、分别称为根结点的左子树和右子树组成):二叉树即是每个节点最多包含左子节点与右子节点这两个节点的树形数据结构。
- 满二叉树::树中的每个节点仅包含 0 或 2 个节点。
- 完美二叉树(Perfect Binary Tree): 二叉树中的每个叶节点都拥有两个子节点,并且具有相同的高度。
- 完全二叉树:除最后一层外,每一层上的结点数均达到最大值;在最后一层上只缺少右边的若干结点。
- 二叉搜索树:左子树的所有节点的值均小于它的根节点的值,右子树的所有节点的值均大于它的根节点的值,它的左右子树也分别为二叉搜索树。
- 平衡二叉树:左右两个子树的高度差绝对值不超过1,且左右两个子树都是平衡二叉树;通过左旋右旋来实现平衡;
- 红黑树:(1)每个节点或者是黑色,或者是红色。(2)根节点是黑色。(3)如果一个节点是红色的,则它的子节点必须是黑色的。(4)从一个节点到该节点的叶子节点的所有路径上包含相同数目的黑节点。(5)不存在连续两个的红色节点(6)叶子结点为黑色(null/nil也是黑色节点)
6、Heap
- 堆(也被称为优先队列(队列+排序规则)是一种特殊的基于树的满足某些特性的数据结构,整个堆中的所有父子节点的键值都会满足相同的排序条件。
- 堆更准确地可以分为最大堆与最小堆,在最大堆中,父节点的键值永远大于或者等于子节点的值,并且整个堆中的最大值存储于根节点;而最小堆中,父节点的键值永远小于或者等于其子节点的键值,并且整个堆中的最小值存储于根节点。
7、Hashing
- 哈希能够将任意长度的数据映射到固定长度的数据。哈希函数返回的即是哈希值,如果两个不同的键得到相同的哈希值,即将这种现象称为碰撞。
- HashMap: Hash Map 是一种能够建立起键与值之间关系的数据结构,Hash Map能够使用哈希函数将键转化为桶或者槽中的下标,从而优化对于目标值的搜索速度。
- 碰撞解决:(1)链地址法(Separate Chaining):链地址法中,每个桶是相互独立的,包含了一系列索引的列表。搜索操作的时间复杂度即是搜索桶的时间(固定时间)与遍历列表的时间之和。(2)开地址法(Open Addressing):在开地址法中,当插入新值时,会判断该值对应的哈希桶是否存在,如果存在则根据某种算法依次选择下一个可能的位置,直到找到一个尚未被占用的地址。所谓开地址法也是指某个元素的位置并不永远由其哈希值决定。
8、Graph
- 图是一种数据元素间为多对多关系的数据结构,加上一组基本操作构成的抽象数据类型。
- 无向图(Undirected Graph):无向图具有对称的邻接矩阵,因此如果存在某条从节点 u 到节点 v 的边,反之从 v 到 u 的边也存在。
- 有向图(Directed Graph): 有向图的邻接矩阵是非对称的,即如果存在从 u 到 v 的边并不意味着一定存在从 v 到 u 的边。
三、算法
比较类排序:通过比较来决定元素间的相对次序,由于其时间复杂度不能突破O(nlogn),因此也称为非线性时间比较类排序。
非比较类排序:不通过比较来决定元素间的相对次序,它可以突破基于比较排序的时间下界,以线性时间运行,因此也称为线性时间非比较类排序
- 稳定:如果a原本在b前面,而a=b,排序之后a仍然在b的前面。
- 不稳定:如果a原本在b的前面,而a=b,排序之后 a 可能会出现在 b 的后面。
- 时间复杂度:对排序数据的总的操作次数。反映当n变化时,操作次数呈现什么规律。
- 空间复杂度:是指算法在计算机 内执行时所需存储空间的度量,它也是数据规模n的函数
1、冒泡排序
冒泡排序是一种简单的排序算法。它重复地走访过要排序的数列,一次比较两个元素,如果它们的顺序错误就把它们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端。
1.1 算法描述
- 比较相邻的元素。如果第一个比第二个大,就交换它们两个;
- 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对,这样在最后的元素应该会是最大的数;
- 针对所有的元素重复以上的步骤,除了最后一个;
- 重复步骤1~3,直到排序完成。
1.2 代码实现
public static int[] bubbleSort(int[] arr) {
if (arr == null || arr.length < 2) {
return arr;
}
//记录最后一次交换的位置
int lastExchangeIndex = 0;
//无序数列的边界,每次比较只需要比到这里为止
int sortBorder = arr.length - 1;
for (int i = 0; i < arr.length - 1; i++) {
boolean isSorted = true;//有序标记,每一轮的初始是true
for (int j = 0; j < sortBorder; j++) {
if (arr[j + 1] < arr[j]) {
isSorted = false;//有元素交换,所以不是有序,标记变为false
int t = arr[j];
arr[j] = arr[j+1];
arr[j+1] = t;
lastExchangeIndex = j;
}
}
sortBorder = lastExchangeIndex
//一趟下来是否发生位置交换,如果没有交换直接跳出大循环
if(isSorted )
break;
}
return arr;
}
2、快速排序
快速排序的基本思想:通过一趟排序将待排记录分隔成独立的两部分,其中一部分记录的关键字均比另一部分的关键字小,则可分别对这两部分记录继续进行排序,以达到整个序列有序。分而治之的思想。
2.1 算法描述
快速排序使用分治法来把一个串(list)分为两个子串(sub-lists)。具体算法描述如下:
- 从数列中挑出一个元素,称为 “基准”(pivot);
- 重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作;
- 递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序。
2.2 算法图解
快速排序最核心的地方在于一趟快速排序过程。一趟快速排序的具体步骤是(以从小到大排序为例):
附设两个指针 left 和 right,它们初始分别指向待排序序列的左端和右端;此外还要附设一个基准元素 pivot(一般选取第一个,本例中初始 pivot 的值为 20)。
首先从 right 所指的位置从右向左搜索找到第一个小于 pivot 的元素,然后将其记录在基准元素所在的位置。
接着从 left 所指的位置从左向右搜索找到第一个大于 pivot 的元素,然后将其记录在 right 所指向的位置。
然后再从 right 所指向的位置继续从右向左搜索找到第一个小于 pivot 的元素,然后将其记录在 left 所指向的位置。
接着,left 继续从左向右搜索第一个大于 pivot 的元素,如果在搜索过程中出现了 left == right ,则说明一趟快速排序结束。此时将 pivot 记录在 left 和 right 共同指向的位置即可。
上述便是一轮快速排序的过程。
2.3 代码实现
/**
* 快速排序
*/
public static void quickSort(int[] arr, int left, int right){
if (left < right){
// 把数组分块
int pivot = partition(arr, left, right);
System.out.println(Arrays.toString(arr));
// 基准元素左边递归
quickSort(arr, left, pivot-1);
// 基准元素右边递归
quickSort(arr, pivot+1, right);
}
}
public static int partition(int[] arr, int left, int right){
int pivot = arr[left]; // 选取第一个为基准元素
while(left<right){
/* 先从右往移动,直到遇见小于 pivot 的元素 */
while (left<right && arr[right]>=pivot){
right--;
}
arr[left] = arr[right]; // 记录小于 pivot 的值
/* 再从左往右移动,直到遇见大于 pivot 的元素 */
while(left<right && arr[left]<=pivot){
left++;
}
arr[right] = arr[left]; // 记录大于 pivot 的值
}
arr[left] = pivot; // 记录基准元素到当前指针指向的区域
return left; // 返回基准元素的索引
}
3、插入排序
插入排序(Insertion-Sort)的算法描述是一种简单直观的排序算法。它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。
3.1 算法描述
一般来说,插入排序都采用in-place在数组上实现。具体算法描述如下:
- 从第一个元素开始,该元素可以认为已经被排序;
- 取出下一个元素,在已经排序的元素序列中从后向前扫描;
- 如果该元素(已排序)大于新元素,将该元素移到下一位置;
- 重复步骤3,直到找到已排序的元素小于或者等于新元素的位置;
- 将新元素插入到该位置后;
- 重复步骤2~5。
3.2 算法图解
给定无序数组如下:
把数组的首元素5作为有序区,此时有序区只有这一个元素:
第一轮
让元素8和有序区的元素依次比较。8>5,所以元素8和元素5无需交换。此时有序区的元素增加到两个:
第二轮
让元素6和有序区的元素依次比较。6<8,所以把元素6和元素8进行交换:
6>5,所以把元素6和元素5无需交换。此时有序区的元素增加到三个:
第三轮
让元素3和有序区的元素依次比较。3<8,所以把元素3和元素8进行交换:
3<6,所以把元素3和元素6进行交换:
3<5,所以把元素3和元素5进行交换:
此时有序区的元素增加到四个:
以此类推,插入排序一共会进行(数组长度-1)轮,每一轮的结果如下:
优化:
在第三轮操作中,我们需要让元素3逐个与有序区的元素进行比较和交换,与8交换、与6交换、与5交换,最终交换到有序区的第一个位置。
但是我们并不需要真的进行完整交换,只需把元素3暂存起来,再把有序区的元素从左向右逐一复制。
第一步,暂存元素3:
第二步,和前一个元素比较,由于3<8,复制元素8到它下一个位置:
第三步,和前一个元素比较,由于3<6,复制元素6到它下一个位置:
第四步,和前一个元素比较,由于3<5,复制元素5到它下一个位置:
第五步,也是最后一步,把暂存的元素3赋值到数组的首位:
显然,这样的优化方法减少了许多无谓的交换。
3.3 代码实现
public static void insertSort(int[] arr){
for (int i = 1; i<arr.length; i++){//①
for (int j = i-1; j>=0; j--){//②
if(arr[j]>arr[j+1]){
int temp = arr[j];
arr[j] = arr[j+1];
arr[j+1] = temp;
}else
break;
}
}
System.out.println(Arrays.toString(arr));
}
优化后:
public static void insertSort(int[] arr){
for (int i = 1; i < arr.length; i++) {
int insertValue = arr[i];
int j = i - 1;
//从右向左比较元素的同时,进行元素复制
for(; j >=0 && insertValue < arr[j]; j--){
arr[j+1] = arr[j];
}
//insertValue的值插入适当位置
arr[j+1] = insertValue;
}
}
4、希尔排序
1959年Shell发明,第一个突破O(n2)的排序算法,是简单插入排序的改进版。它与插入排序的不同之处在于,它会优先比较距离较远的元素。希尔排序又叫缩小增量排序。
4.1 算法描述
先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,具体算法描述:
- 选择一个增量序列t1,t2,…,tk,其中ti>tj,tk=1;
- 按增量序列个数k,对序列进行k 趟排序;
- 每趟排序,根据对应的增量ti,将待排序列分割成若干长度为m 的子序列,分别对各子表进行直接插入排序。仅增量因子为1
时,整个序列作为一个表来处理,表长度即为整个序列的长度。
4.2 算法图解
假设原始数组arr数据如下
初始增量gap=arr.length/2=8/2=4,意味着整个数组被分为4组,这四组分别使用插入排序如下
继续进行增量排序gap=gap/2=4/2=2,数组被重新分组为两组,对这两组使用插入排序继续进行排序
直到增量排序gap=1时,将数组分为一组,数组无法再进行增量排序,则进行最后一次排序
经过最后一次的排序,整体宏观调控,微型调整,即可完成排序
4.3 代码实现
public void exchangeShellSort(int arr[]) {
int temp;//临时数据
boolean flag = false;//是否交换
int count = 1;//计数
// 分而治之,将数值分组排序,i为步长
for (int i = arr.length / 2; i > 0; i /= 2) {
// 遍历分治的每一个分组
for (int j = i; j < arr.length; j++) {
// 遍历分治的每一个分组的每一个值
for (int k = j - i; k >= 0; k -= i) {
if (arr[k + i] < arr[k]) {
temp = arr[k + i];
arr[k + i] = arr[k];
arr[k] = temp;
flag = true;
}
if (!flag) {
break;
} else {
// 为了下次判断
flag = false;
}
}
}
System.out.println("希尔排序交换法第" + (count++) + "次排序后" + Arrays.toString(arr));
}
}
5、选择排序
选择排序(Selection-sort)是一种简单直观的排序算法。它的工作原理:首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。
5.1 算法描述
n个记录的直接选择排序可经过n-1趟直接选择排序得到有序结果。具体算法描述如下:
- 初始状态:无序区为R[1…n],有序区为空;
- 第i趟排序(i=1,2,3…n-1)开始时,当前有序区和无序区分别为R[1…i-1]和R(i…n)。该趟排序从当前无序区中-选出关键字最小的记录R[k],将它与无序区的第1个记录R交换,使R[1…i]和R[i+1…n)分别变为记录个数增加1个的新有序区和记录个数减少1个的新无序区;
- n-1趟结束,数组有序化了。
5.2 代码实现
public static void selectSort(int[] arr){
//从无序区间不断挑选出最小值,挑选 n-1 次
for (int i = 0; i < arr.length - 1; i++) {
int minIndex = i;
for (int j = i + 1; j < arr.length; j++) {
//更换最小值下标
minIndex = arr[minIndex] < arr[j] ? minIndex : j;
}
//交换最小值与无序区间第一个元素
int temp = arr[i];
arr[i] = arr[minIndex];
arr[minIndex] = temp;
}
}
6、堆排序
堆排序(Heapsort)是指利用堆这种数据结构所设计的一种排序算法。堆积是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点,俗称大(小)顶堆。
大顶堆特点:arr[i] >= arr[2i+1] && arr[i] >= arr[2i+2] // i 对应第几个节点,i从0开始编号
小顶堆:arr[i] <= arr[2i+1] && arr[i] <= arr[2i+2] // i 对应第几个节点,i从0开始编号
一般升序采用大顶堆,降序采用小顶堆
6.1 算法描述
- 将初始待排序关键字序列(R1,R2….Rn)构建成大顶堆,此堆为初始的无序区;
- 将堆顶元素R[1]与最后一个元素R[n]交换,此时得到新的无序区(R1,R2,……Rn-1)和新的有序区(Rn),且满足R[1,2…n-1]<=R[n];
- 由于交换后新的堆顶R[1]可能违反堆的性质,因此需要对当前无序区(R1,R2,……Rn-1)调整为新堆,然后再次将R[1]与无序区最后一个元素交换,得到新的无序区(R1,R2….Rn-2)和新的有序区(Rn-1,Rn)。不断重复此过程直到有序区的元素个数为n-1,则整个排序过程完成。
6.2 步骤图解
假设给定无序序列结构如下
步骤一此时我们从最后一个非叶子结点开始(叶结点自然不用调整,第一个非叶子结点arr.length/2-1=5/2-1=1,也就是下面的6结点),从右至左,从下至上进行调整。
找到第二个非叶节点4,由于[4,9,8]中9元素最大,4和9交换。
这时,交换导致了子根[4,5,6]结构混乱,继续调整,[4,5,6]中6最大,交换4和6。
此时,我们就将一个无序序列构造成了一个大顶堆。
步骤二将堆顶元素与末尾元素进行交换,使末尾元素最大。然后继续调整堆,再将堆顶元素与末尾元素交换,得到第二大元素。如此反复进行交换、重建、交换
将堆顶元素9和末尾元素4进行交换
重新调整结构,使其继续满足堆定义
再将堆顶元素8与末尾元素5进行交换,得到第二大元素8
后续过程,继续进行调整,交换,如此反复进行,最终使得整个序列有序
6.3 代码实现
package com.qf.sort;
import java.util.Arrays;
public class HeapSort {
public static void main(String[] args) {
int[] arr={4,6,8,5,9};
heapSort(arr);
System.out.println(Arrays.toString(arr));
}
public static void heapSort(int[] arr){
/*
将待排序序列构造成一个大顶堆
此时,整个序列的最大值就是堆顶的根节点。*/
for (int i = arr.length/2-1; i >=0; i--) {
adjustHeap( arr,i,arr.length);
}
int temp=0;
/*
将其与末尾元素进行交换,此时末尾就为最大值。
然后将剩余n-1个元素重新构造成一个堆,这样会得到n个元素的次小值。如此反复执行,便能得到一个有序序列了。*/
for (int j=arr.length-1;j>0;j--){
temp=arr[j];
arr[j]=arr[0];
arr[0]=temp;
adjustHeap(arr,0,j);
}
}
public static void adjustHeap(int[] arr,int i,int length){
//第一步把数组 {4,6,8,5,9}调整排序为{4,9,8,5,6}
//k=i*2+1 是循环对应的左子树元素
//记录当前要调整的元素
int temp=arr[i];
for (int k=i*2+1;k<length;k=k*2+1){
//把当前左子树和右子树的数据作比较,更换较大的数据
if (k+1<length&&arr[k]<arr[k+1]){
k++;
}
if (arr[k]>temp){
//交换位置
arr[i]=arr[k];
i=k;
}else{
break;
}
}
arr[i]=temp;
}
}
7、归并排序
归并排序是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为2-路归并。 同时在归并排序中还用到了一个算法,就是有序数组合并算法。配合递归与有序数组合并算法,归并排序能够高效且稳定的完成排序,归并排序的优点在于其时间复杂度低,稳定性高,但是缺点也是有的,那就是空间复杂度很高。
7.1 算法描述
- 把长度为n的输入序列分成两个长度为n/2的子序列;
- 对这两个子序列分别采用归并排序;
- 将两个排序好的子序列合并成一个最终的排序序列。
7.2 步骤图解
我们首先来详细说说归并排序的算法思路,归并排序的算法思路并不复杂,其主要是一个拆分与合并的过程,接下来我们用图解来看看归并排序究竟是如何排序的。
首先,我们得到了这样的一个数组:
之后我们将其进行一次按照中间位置一分为二的划分:
之后我们在此基础上再为这两个被划分出来的数组进行进一步划分:
只要每个数组长度大于1,那么我们就会继续划分,因此在上图中的情况下我们仍然要继续划分,如图所示:
被划分成这个状态之后,我们便不再划分,而是两两将其进行有序数组的拼接,如图所示:
在此拼接的基础上我们继续拼接,只要这个数组还是被划分为多个子数组的状态我们就会一直继续拼接,下次拼接的结果如图所示:
我们继续拼接,如下图,发现整个数组又变成一个了,拼接完成:
7.3 代码实现
import java.util.*;
class Untitled {
public static void main(String[] args) {
int[] arr = {5,7,4,2,0,3,1,6};
mergeSort(arr, 0, arr.length-1);
System.out.print(Arrays.toString(arr));
}
public static void mergeSort(int[] arr,int left,int right){
if(left>=right){
return;
}
int mid = (left + right) / 2;
mergeSort(arr, left, mid);
mergeSort(arr, mid+1, right);
merge(arr, left, mid, right);
}
//需要注意的是整个合并过程中并没有将两个被合并的数组单独拎出来,二者始终是存在于一个数组地址上的
public static void merge(int[] arr,int left,int mid,int right){
int s1 = left;//根据拿到的左边界,我们定其为第一个数组的指针
int s2 = mid+1;//根据中间位置,让中间位置右移一个单位,那就是第二个数组的指针
int[] temp = new int[right - left+1];//根据左右边界相减我们得到这片空间的长度,以此声明额外空间
int i = 0;//定义额外空间的指针
while(s1<=mid && s2 <=right){
if(arr[s1]<=arr[s2]){//如果第一个数组的指针数值小于第二个数组的,那么其放置在临时空间上
temp[i++] = arr[s1++];
}else{//否则是第二个数组的数值放置于其上
temp[i++] = arr[s2++];
}
}
while(s1<=mid){//如果这是s1仍然没有到达其终点,那么说明它还有剩
temp[i++] = arr[s1++];//因为我们知道每个参与合并的数组都是有序数组,因此直接往后拼接即可
}
while(s2<=right){//同上
temp[i++] = arr[s2++];
}
for(int j = 0;j<temp.length;j++){//数组复制
arr[j+left] = temp[j];
}
}
}
8、计数排序
计数排序不是基于比较的排序算法,其核心在于将输入的数据值转化为键存储在额外开辟的数组空间中。 作为一种线性时间复杂度的排序,计数排序要求输入的数据必须是有确定范围的整数。
计数排序是一个稳定的排序算法。当输入的元素是 n 个 0到 k 之间的整数时,时间复杂度是O(n+k),空间复杂度也是O(n+k),其排序速度快于任何比较排序算法。当k不是很大并且序列比较集中时,计数排序是一个很有效的排序算法。
时间复杂度是O(n+k):通过上面的代码可知最终的计数算法花费的时间是3n+k,则时间复杂度是O(n+k)。
空间复杂度是O(k):如果出去最后的返回数组,则空间复杂度是2k,则空间复杂度是O(k)
稳定算法:由于统计数组可以知道该索引在原数组中排第几位,相同的元素其在原数组中排列在后面,其从原数组的后面遍历,其在最终数组中的索引也在后面,所以相同的元素其相对位置不会改变。
8.1 算法描述
- 取无序数组arr中的最大值max和最小值min,新建(max-min +1)长度的数组newArr和长度为(max-min +1)的统计数组countArr;
- 遍历原数组arr,将其值作为newArr的键,元素的个数作为值存放在该键处;
- 遍历newArr,使统计数组countArr和newArr相同索引处存放的是newArr该索引之前元素的和;
- 新建一个最终数组result,反向遍历原数组,取原数组的值arr[i]-min作为索引,从统计数组countArr取出该索引的值减1,作为最终数组result的索引,值为原数组的arr[i],同时统计数组该索引处值减1,遍历结束后,最终数组result为排序后的数组。
8.2 算法图解
先假设 20 个数列为:{9, 3, 5, 4, 9, 1, 2, 7, 8,1,3, 6, 5, 3, 4, 0, 10, 9, 7, 9}。
让我们先遍历这个无序的随机数组,找出最大值为 10 和最小值为 0。这样我们对应的计数范围将是 0 ~ 10。然后每一个整数按照其值对号入座,对应数组下标的元素进行加1操作。
比如第一个整数是 9,那么数组下标为 9 的元素加 1,如下图所示。
第二个整数是 3,那么数组下标为 3 的元素加 1,如下图所示。
继续遍历数列并修改数组…。最终,数列遍历完毕时,数组的状态如下图。
数组中的每一个值,代表了数列中对应整数的出现次数。
有了这个统计结果,排序就很简单了,直接遍历数组,输出数组元素的下标值,元素的值是几,就输出几次。比如统计结果中的 1 为 2,就是数列中有 2 个 1 的意思。这样我们就得到最终排序好的结果。
0, 1, 1, 2, 3, 3, 3, 4, 4, 5, 5, 6, 7, 7, 8, 9, 9, 9, 9, 10
8.3 代码实现
public class CountSort {
public static void main(String[] args) {
int[] arr = {1, 4, 9, 2, 5, 3, 7, 6, 22, 23, 15, 24, 0, 3,
4, 5, 2, 3, 5, 12, 1, 3, 4, 2, 1,
3, 45, 1, 1};
// 计数排序
int[] ints = countSort1(arr);
for (int i = 0; i < ints.length; i++) {
System.out.print(ints[i] + " ");
}
}
private static int[] countSort1(int[] arr) {
// 检测数据
if (arr == null || arr.length == 0) {
return arr;
}
// 缩短不必要的长度,获取最大和最小值
int min = arr[0];
int max = arr[0];
for (int i = 0; i < arr.length; i++) {
if (arr[i] > max) {
max = arr[i];
}
if (arr[i] < min) {
min = arr[i];
}
}
// 用数组中的值作为索引,个数作为值
int[] newArr = new int[(max - min + 1)];
// 用于记录当前数据排在第几位
int[] countArr = new int[newArr.length];
for (int i = 0; i < arr.length; i++) {
newArr[arr[i] - min]++;
}
// 统计数组,记录后面的元素等于前面的元素之和
for (int i = 0; i < newArr.length; i++) {
if (i == 0) {
countArr[i] = newArr[i];
continue;
}
countArr[i] = newArr[i] + countArr[i - 1];
}
// 最终结果
int[] result = new int[arr.length];
// 反向遍历
for (int i = arr.length - 1; i >= 0; i--) {
// 将数据放入指定的位置中,统计数组中记录的是元素排在第几位
result[countArr[arr[i] - min] - 1] = arr[i];
// 排列一个后,就减少一个
countArr[arr[i] - min]--;
}
return result;
}
}
9、桶排序
桶排序是计数排序的升级版。它利用了函数的映射关系,高效与否的关键就在于这个映射函数的确定。桶排序 (Bucket sort)的工作的原理:假设输入数据服从均匀分布,将数据分到有限数量的桶里,每个桶再分别排序(有可能再使用别的排序算法或是以递归方式继续使用桶排序进行排)。核心思想:就是将大问题化小(分治思想)。
桶排序最好情况下使用线性时间O(n),桶排序的时间复杂度,取决与对各个桶之间数据进行排序的时间复杂度,因为其它部分的时间复杂度都为O(n)。很显然,桶划分的越小,各个桶之间的数据越少,排序所用的时间也会越少。但相应的空间消耗就会增大。
9.1 算法描述
- 设置一个定量的数组当作空桶;
- 遍历输入数据,并且把数据一个一个放到对应的桶里去;
- 对每个不是空的桶进行排序;
- 从不是空的桶里把排好序的数据拼接起来。
9.2 实现图鉴
9.3 代码实现
import java.util.ArrayList;
public class BucketSort {
public static void main(String[] args) {
int[] arr = { 1, 45, 32, 23, 22, 31, 47, 24, 4, 15 };
bucketsort(arr);
}
public static void bucketsort(int[] arr) {
ArrayList bucket[] = new ArrayList[5];// 声明五个桶
for (int i = 0; i < bucket.length; i++) {
bucket[i] = new ArrayList<Integer>();// 确定桶的格式为ArrayList
}
for (int i = 0; i < arr.length; i++) {
int index = arr[i] / 10;// 确定元素存放的桶号
bucket[index].add(arr[i]);// 将元素存入对应的桶中
}
for (int i = 0; i < bucket.length; i++) {// 遍历每一个桶
bucket[i].sort(null);// 对每一个桶排序
for (int i1 = 0; i1 < bucket[i].size(); i1++) {// 遍历桶中的元素并输出
System.out.print(bucket[i].get(i1) + " ");
}
}
}
}
10、基数排序
基数排序是将所有待比较数值(自然数)统一为同样的数位长度,数位较短的数前面补零。然后,从最低位开始,依次进行一次排序。这样从最低位排序一直到最高位排序完成以后, 数列就变成一个有序序列。
基数排序基于分别排序,分别收集,所以是稳定的。但基数排序的性能比桶排序要略差,每一次关键字的桶分配都需要O(n)的时间复杂度,而且分配之后得到新的关键字序列又需要O(n)的时间复杂度。假如待排数据可以分为d个关键字,则基数排序的时间复杂度将是O(d*2n) ,当然d要远远小于n,因此基本上还是线性级别的。
基数排序的空间复杂度为O(n+k),其中k为桶的数量。一般来说n>>k,因此额外空间需要大概n个左右。
10.1 算法描述
- 确定数组中的最大元素有几位(MAX)(确定执行的轮数)
- 创建0~ 9个桶(桶的底层是队列),因为所有的数字元素都是由0~9的十个数字组成
- 依次判断每个元素的个位,十位至MAX位,存入对应的桶中,出队,存入原数组;直至MAX轮结束输出数组。
10.2 实现图鉴
10.3 代码实现
import java.util.LinkedList;
public class Radix {
public static void main(String[] args) {
int[] arr = { 23, 1, 4, 9, 98, 132, 42 };
sort(arr);
}
public static void sort(int[] arr) {
// 1.找 分类-收集 的轮数(最大值的长度)
int radix = getRadix(arr);
// 2.创建桶 list所有桶的集合 每一个桶是LinkedList当成队列来用
LinkedList<Integer>[] list = new LinkedList[10];
for (int i = 0; i < list.length; i++) {
list[i] = new LinkedList<>();
}
// 3.开始 分类-收集
for (int r = 1; r <= radix; r++) {
// 分类过程
for (int i = 0; i < arr.length; i++) {
list[getIndex(arr[i], r)].offer(arr[i]);
}
int index = 0; // 遍历arr原数组
// 收集的过程
for (int i = 0; i < list.length; i++) {
while (!list[i].isEmpty()) {
arr[index++] = list[i].poll();
}
}
}
for (int i = 0; i < arr.length; i++) {
System.out.print(arr[i] + " ");
}
}
private static int getIndex(int num, int r) {
int ret = 0;
for (int i = 1; i <= r; i++) {
ret = num % 10;
num /= 10;
}
return ret;
}
private static int getRadix(int[] arr) {
int max = arr[0];
for (int i = 1; i < arr.length; i++) {
if (arr[i] > max) {
max = arr[i];
}
}
return (max + "").length();
}
}