说了这么久的数据结构,理论性比较强,下面我们来进入算法部分,运用之前学的数据结构来实现算法。今天的主体部分是排序,难度不大。
排序
排序的算法是比较简单实用的算法,也是很多的算法的基础。也分很多种,可以根据时间空间难度不同的,有序数据能够被更高效地查找、分析和处理
。
选择排序
选择算法是一个时间复杂度O(n2),空间复杂度是O(1),运行时间比较长。其主要思想是每次从未排序的部分中选择最小(或最大)的元素,将其放在已排序部分的末尾。以下是选择排序的详细步骤:
选择排序的步骤
-
初始状态:
- 给定一个待排序的数组或列表
arr
,长度为n
。 - 将数组分为两部分:已排序部分和未排序部分。最初,已排序部分为空,未排序部分为整个数组。
- 给定一个待排序的数组或列表
-
外层循环:
- 遍历数组,从
arr[0]
到arr[n-2]
,依次执行以下步骤。
- 遍历数组,从
-
寻找最小值:
- 在未排序部分中(从当前索引
i
到n-1
),找到最小元素的索引min_index
。 - 具体地,从
arr[i]
开始,比较arr[i]
、arr[i+1]
、…、arr[n-1]
的值,记录下最小值的索引。
- 在未排序部分中(从当前索引
-
交换元素:
- 找到最小元素后,将其与未排序部分的第一个元素(即
arr[i]
)交换。 - 这样,未排序部分的第一个元素变成了已排序部分的最后一个元素。
- 找到最小元素后,将其与未排序部分的第一个元素(即
-
重复步骤 2 到 4:
- 对于剩下的未排序部分,重复上述过程,直到数组完全排序(即外层循环到达
n-2
)。
- 对于剩下的未排序部分,重复上述过程,直到数组完全排序(即外层循环到达
-
完成排序:
- 当外层循环结束时,数组已被排序。
public class SelectionSort {
// 方法:选择排序
public static void selectionSort(int[] arr) {
int n = arr.length;
// 外层循环:逐步缩小未排序部分的范围
for (int i = 0; i < n - 1; i++) {
// 假设当前元素为未排序部分的最小值
int minIndex = i;
// 内层循环:找到未排序部分的最小元素
for (int j = i + 1; j < n; j++) {
if (arr[j] < arr[minIndex]) {
minIndex = j;
}
}
// 如果找到的最小元素不是当前元素,则交换
if (minIndex != i) {
int temp = arr[minIndex];
arr[minIndex] = arr[i];
arr[i] = temp;
}
}
}
//测试选择排序
public static void main(String[] args) {
int[] arr = {64, 25, 12, 22, 11};
System.out.println("排序前的数组:");
printArray(arr);
selectionSort(arr);
System.out.println("排序后的数组:");
printArray(arr);
}
//打印数组
public static void printArray(int[] arr) {
for (int j : arr) {
System.out.print(j + " ");
}
System.out.println();
}
}
冒泡排序
冒泡排序(Bubble Sort)是一种简单的排序算法,它的基本思想是通过多次遍历列表,不断地将相邻的元素进行比较并交换,使得每一轮遍历之后,最大的元素会“冒泡”到数组的末尾。这个过程会重复进行,直到整个数组排序完成。下面是对冒泡排序的详细解释:
冒泡排序的工作原理
-
初始状态:
- 假设我们有一个未排序的数组或列表,长度为
n
。
- 假设我们有一个未排序的数组或列表,长度为
-
外层循环:
- 外层循环控制排序的轮数,共需要
n-1
轮,因为在最坏的情况下,所有元素都需要比较n-1
次才能确定其最终位置。
- 外层循环控制排序的轮数,共需要
-
内层循环:
- 在每一轮排序中,内层循环负责在未排序的部分中两两比较相邻的元素。
- 如果当前元素比下一个元素大,则交换它们的位置,这样大的元素逐渐移动到列表的末尾(像气泡一样上升)。
- 经过一轮比较后,最大的元素会被移动到数组的末尾。
-
逐步缩小范围:
- 每一轮排序后,已经排好序的部分不再参与后续的比较和交换,因此内层循环的范围逐渐缩小。
-
结束条件:
- 当不再需要交换时,排序结束。虽然算法设计上要求
n-1
轮比较,但一旦发现某一轮没有发生交换,就可以提前终止排序。
- 当不再需要交换时,排序结束。虽然算法设计上要求
冒泡排序的时间复杂度
-
最坏情况:O(n2) —— 当数组是逆序时,算法需要执行最多次数的比较和交换。
-
最好情况:O(n) —— 当数组已经排序时,算法只需一次遍历即可结束。
-
平均情况:O(n^2) —— 大多数情况下,需要执行比较次数与最坏情况相似。
- 实现简单,代码容易理解。
- 对于几乎已经排序好的数组,它可以在更少的时间内完成排序。
插入排序
插入排序(Insertion Sort)是一种简单且直观的排序算法,特别适合于小规模数据的排序。它的基本思想是将待排序的元素逐个插入到已经排好序的部分,直到整个序列有序。插入排序的工作方式类似于我们打牌时将新牌插入到已排序的牌中。
插入排序的工作原理
-
取第一个为基准值:
- 假设我们有一个未排序的数组,该数组的第一个元素可以被视为已经排好序的部分。
-
逐步插入:
- 从第二个元素开始,将其与已经排好序的部分进行比较。
- 如果当前元素小于已排序部分的元素,就将已排序部分的元素向后移动一位,直到找到当前元素应该插入的位置。
-
插入元素:
- 将当前元素插入到找到的位置,继续处理下一个元素,直到所有元素都被插入到已排序部分中。
下面是使用 Java 实现的插入排序的完整代码示例:
public class InsertionSort {
// 方法:插入排序
public static void insertionSort(int[] arr) {
int n = arr.length;
// 外层循环:从第二个元素开始,遍历到最后一个元素
for (int i = 1; i < n; i++) {
int key = arr[i]; // 当前待插入元素
int j = i - 1;
// 内层循环:将已排序部分的元素向后移动,找到插入位置
while (j >= 0 && arr[j] > key) {
arr[j + 1] = arr[j]; // 将大于 key 的元素向后移动
j--;
}
arr[j + 1] = key; // 插入当前元素到找到的位置
}
}
// 主方法:测试插入排序
public static void main(String[] args) {
int[] arr = {5, 2, 9, 1, 5, 6};
System.out.println("排序前的数组:");
printArray(arr);
insertionSort(arr);
System.out.println("排序后的数组:");
printArray(arr);
}
// 辅助方法:打印数组
public static void printArray(int[] arr) {
for (int j : arr) {
System.out.print(j + " ");
}
System.out.println();
}
}
代码说明
-
insertionSort
方法:- 接收一个整数数组
arr
,并对其进行排序。 - 外层循环从
1
开始遍历到数组的最后一个元素,key
变量存储当前待插入的元素。 - 内层循环向后移动已排序部分的元素,直到找到
key
应该插入的位置。
- 接收一个整数数组
-
数组元素移动:
- 当已排序部分的元素大于
key
时,将该元素向后移动一位,以便为key
腾出插入的位置。
- 当已排序部分的元素大于
-
插入元素:
- 在内层循环结束后,
j + 1
就是key
应该插入的位置。
- 在内层循环结束后,
-
printArray
方法:- 辅助方法,用于打印数组的内容。
插入排序的特点
-
时间复杂度:
- 最坏情况:
O(n^2)
(当数组逆序时)。 - 最好情况:
O(n)
(当数组已经排序时)。 - 平均情况:
O(n^2)
。
- 最坏情况:
-
空间复杂度:
O(1)
,因为它是原地排序算法。 -
稳定性:插入排序是稳定的排序算法,即相等元素的相对顺序不会改变。
插入排序在小规模数据集上表现良好,并且在数据部分有序时效率较高。
快速排序
快速排序(Quicksort)是一种高效的排序算法,最早由英国计算机科学家托尼·霍尔提出。它采用了分治策略
,将一个数组分成两个子数组,并递归地对这两个子数组进行排序。快速排序在平均情况下具有非常好的性能,其时间复杂度为 (O(n log n))。分治算法就是将问题进行切片
,牺牲空间换取时间。
快速排序的基本思想
快速排序通过以下步骤来对数组进行排序:
-
选择基准(Pivot):
- 从数组中选择一个元素作为基准。基准的选择方法有很多,常见的有选择第一个元素、最后一个元素、中间元素,或随机选择一个元素等。
-
分区(Partitioning):
- 通过一趟排序将数组分为两部分:左边部分的元素都小于等于基准,右边部分的元素都大于等于基准。
-
递归排序:
- 就是将这个进行二分树状分割,重复排序分割的部分,最后没有需要排序的就返回(0或1)
快速排序的实现
下面是使用 Java 实现快速排序的代码示例:
public class QuickSort {
// 方法:快速排序
public static void quickSort(int[] arr, int low, int high) {
if (low < high) {
// 获取分区点(pivot)
int pi = partition(arr, low, high);
// 递归排序左半部分
quickSort(arr, low, pi - 1);
// 递归排序右半部分
quickSort(arr, pi + 1, high);
}
}
// 分区方法:返回分区点
private static int partition(int[] arr, int low, int high) {
int pivot = arr[high]; // 选择最右边的元素作为基准
int i = (low - 1); // i 指向小于 pivot 的最后一个元素
for (int j = low; j < high; j++) {
// 如果当前元素小于或等于 pivot
if (arr[j] <= pivot) {
i++;
// 交换 arr[i] 和 arr[j]
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}
// 将 pivot 放置到正确的位置
int temp = arr[i + 1];
arr[i + 1] = arr[high];
arr[high] = temp;
return i + 1;
}
// 主方法:测试快速排序
public static void main(String[] args) {
int[] arr = {10, 7, 8, 9, 1, 5};
System.out.println("排序前的数组:");
printArray(arr);
quickSort(arr, 0, arr.length - 1);
System.out.println("排序后的数组:");
printArray(arr);
}
// 辅助方法:打印数组
public static void printArray(int[] arr) {
for (int j : arr) {
System.out.print(j + " ");
}
System.out.println();
}
}
代码解析
-
quickSort
方法:- 该方法通过递归的方式对数组进行排序。
low
和high
分别代表当前排序范围的起始和结束索引。
- 该方法通过递归的方式对数组进行排序。
-
partition
方法:- 这是快速排序的核心步骤。它通过选择一个基准元素(这里选择数组的最后一个元素),然后调整数组元素的位置,使得基准元素的左边都小于或等于它,右边都大于它。最后返回基准元素的位置索引。
-
递归:
- 在
quickSort
方法中,排序完成后,基准元素将数组分为两个部分,算法会继续递归地对这两个部分进行快速排序。
- 在
快速排序的时间复杂度
- 最坏时间复杂度:(O(n2))(当数组已经有序,且每次选择的基准是最小或最大的元素时)。
- 最好时间复杂度:(O(n log n))(当每次分区都恰好将数组平分时)。
- 平均时间复杂度:(O(n log n))(快速排序的期望复杂度通常是 (log n) 层递归,每层操作需要 (O(n)))。
空间复杂度
- 空间复杂度:(O(\log n))(主要是由于递归调用栈的深度)。
特点
- 快速排序通常在实践中非常高效,尤其适用于大型且无序的数组。
- 它是不稳定的排序算法,即相同元素的相对位置在排序后可能会改变。
快速排序由于其高效的时间复杂度和较低的空间复杂度,通常被认为是最佳的通用排序算法之一,广泛应用于实际中。
大概一般的排序就这些,学一下思想就好了,大多数时候学Java里面的我用的大多数时候都是sort解决。
持续更新中~~