- 冒泡排序(Bubble Sort)
冒泡排序是一种简单的排序算法,它重复地遍历要排序的数列,一次比较两个元素,如果它们的顺序错误就交换位置。这个过程持续对数列的末尾进行,直到整个数列都排序完成。
public void swap(int[] arr,int a, int b){
int temp =arr[a];
arr[a] = arr[b];
arr[b] = temp;
}
//1.冒泡排序:先排最大的 O(n^2)
public void bubbleSort(int[] arr){
for (int i = 0; i < arr.length; i++) {
for (int j = 0; j < arr.length-1; j++) {
if (arr[j] > arr[j+1]){
swap(arr,j,j+1);
}
}
}
}
冒泡排序的时间复杂度为O(n^2),这使得它在大型列表和实际应用中效率低下。但是,由于其简单性,它是向初学者教授排序的好算法。
2. 选择排序(Selection Sort)
选择排序是一种简单的排序算法,它的基本思想是每次从待排序的元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到全部待排序的元素排完。
//2.选择排序:先找最小的后交换 O(n^2)
public void selectSort(int[] arr){
int length = arr.length;
for (int i = 0; i < length; i++) {
int minIndex = i;
for (int j = i+1; j < length; j++) {
if (arr[j] < arr[minIndex]){
minIndex = j;
}
}
swap(arr,i,minIndex);
}
}
选择排序的时间复杂度为O(n^2),这使得它在大型列表和实际应用中效率低下。但是,由于其简单性,它是向初学者教授排序的好算法。
- 插入排序(Insertion Sort)
插入排序是一种简单的排序算法,它的基本思想是将一个记录插入到已经排好序的有序表中,从而得到一个新的、记录数增1的有序表。
public void insertSort(int[] arr) {
int n = arr.length;
// 初始时,把第一个元素作为已排序好的序列 arr[0],从第二个元素:arr[1]开始
for (int i = 1; i < n; i++) {
int curVal = arr[i];//当前值
// 将当前值和前面的值进行比较,
// 如果前面的值大于curVal 则将值往后移1位再进行比较
int j = i - 1;
while (j >= 0 && arr[j] > curVal) {
arr[j + 1] = arr[j];
j--;
}
//在不小当前值curVal的位置,插入当前值curVal:相当于大于curVal的整体往后移
arr[j + 1] = curVal;
}
}
- 希尔排序(Shell Sort)
希尔排序是一种改进的插入排序算法,它的基本思想是将待排序的数组按照一定的间隔进行分组,对每组使用插入排序算法进行排序,然后缩小间隔,再对分组进行排序,直到间隔为1为止。
逐渐减小间隔大小的方法有助于提高排序过程的效率,可以减少比较和交换的次数。这是希尔排序算法的一个关键特点。
//4.希尔排序 O(n^2) 通过间隔值进行分组
public void shellSort(int[] arr) {
int n = arr.length;
// 初始化间隔(gap)的值,它决定了每次迭代中子数组的大小
// 从数组长度的一半开始作为初始间隔值,gap就是分割的子数组数量
for (int gap = n >> 1; gap > 0; gap = gap >> 1) {
// 循环从间隔值开始,遍历数组直到数组的末尾;代表循环所有的子数组
for (int i = gap; i < n; i++) {
int temp = arr[i];
int j = i;
// 将当前元素 arr[j] 的值替换为前一个元素 arr[j - gap] 的值。
// 通过这个操作,将较大的元素向后移动,为当前元素腾出位置
while (j >= gap && arr[j - gap] > temp) {
arr[j] = arr[j - gap];
j -= gap;
}
arr[j] = temp;
}
}
}
希尔排序的时间复杂度为O(n^2),但实际上它的性能比插入排序要好得多,特别是在大型列表上。希尔排序的性能取决于间隔序列的选择,但是目前还没有一种最优的间隔序列。
- 归并排序(Merge Sort)
归并排序是一种分治思想的排序算法,它的基本思想是将待排序的数组分成若干个子序列,每个子序列都是有序的,然后再将子序列合并成一个有序的数组。
//5.归并排序 O(nlogn)
public void mergeSort(int[] arr, int left, int right) {
if (left < right) {
int mid = left+(right-left)/2;
mergeSort(arr, left, mid);
mergeSort(arr, mid + 1, right);
merge(arr, left, mid, right);
}
}
public void merge(int[] arr, int left, int mid, int right) {
// 左子数组 L 的大小(mid放在左子数组)
int n1 = mid - left + 1;
// 右子数组 R 的大小
int n2 = right - mid;
// 创建两个临时数组 L 和 R ,分别用来存储左子数组和右子数组的元素
int[] L = new int[n1];
int[] R = new int[n2];
// 使用 for 循环将原始数组 arr 中的元素复制到临时数组 L 和 R 中,分别从 left 和 mid + 1 开始
for (int i = 0; i < n1; i++) {
L[i] = arr[left + i];
}
for (int j = 0; j < n2; j++) {//加一跳过mid
R[j] = arr[mid + 1 + j];
}
// 初始化三个变量 i、j和k,分别指向数组 L 、R 和原始数组 arr 的起始位置
int i = 0, j = 0, k = left;
// 使用 while 循环,比较 L 和 R 的元素,并将较小的元素放回原始数组 arr 中
while (i < n1 && j < n2) {
arr[k++] = L[i] <= R[j] ? L[i++]:R[j++];
}
// 当 L 或 R 中的元素用完时,将剩余的元素依次放回原始数组 arr 中
while (i < n1) {
arr[k++] = L[i++];
}
while (j < n2) {
arr[k++] = R[j++];
}
// merge 方法执行完毕后,两个子数组范围内的元素已经按照从小到大的顺序合并到了原始数组 arr 中
}
归并排序的时间复杂度为O(nlogn),它的性能比冒泡排序和插入排序要好得多,特别是在大型列表上。
6. 快速排序(Quick Sort)
快速排序是一种分治思想的排序算法,它的基本思想是通过一趟排序将待排记录分隔成独立的两部分,其中一部分记录的关键字均比另一部分记录的关键字小,然后再分别对这两部分记录继续进行排序,以达到整个序列有序的目的。
/6.快速排序:双指针外加基准值(一般选初始值)
public void quickSort(int[] arr,int left,int right){
if (left >= right ){//递归结束
return;
}
int l = left;
int r = right;
int baseNum = arr[left];
while (l < r){
while (l < r && arr[l] < baseNum)l++;
while (l < r && arr[r] > baseNum)r--;
if (l < r)swap(arr,l,r);
}
quick_sort(arr,left,r-1);
quick_sort(arr,r+1,right);
}
快速排序的时间复杂度为O(nlogn),它的性能比冒泡排序和插入排序要好得多,特别是在大型列表上。
- 堆排序(Heap Sort)
堆排序是一种树形选择排序算法,它的基本思想是将待排序的数组构建成一个大根堆(或小根堆),然后将堆顶元素与堆底元素交换位置,再将剩余元素重新构建成堆,重复执行交换和重构堆的操作,直到整个数组有序。
堆排序是一种基于堆数据结构的排序算法,它的时间复杂度为O(nlogn)。
堆的概念
集合K = {k0,k1, k2,…,kn-1},把它的所有元素按完全二叉树的顺序存储方式存储在一个一维数组中,并满足:Ki <= K2i+1(左节点) 且 Ki<=K2i+2(右节点),则称为小堆(或大堆)。将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆。完全二叉树(除了最后一层以外上面的节点但是非空的,最后一层节点是从左到右依次排布的)
堆的性质
1.堆中某个节点的值总是不大于或不小于其父节点的值;
2.堆总是一棵完全二叉树。
完全二叉树
完全二叉树的特点:叶子结点只能出现在最下层和次下层,且最下层的叶子结点集中在树的左部。需要注意的是,满二叉树肯定是完全二叉树,而完全二叉树不一定是满二叉树。
完全二叉树的第 i 层至多有 2^i - 1 个结点。
完全二叉树的第 i 层至少有 2^(i - 1) 个结点。
完全二叉树的叶子结点只出现在最底层和次底层。
完全二叉树每一层的结点个数都达到了最大值
public void heapSort(int[] arr) {
int temp = 0;
//将无序序列构建成一个堆,根据升序降序需求选择大顶堆或小顶堆
for (int i = arr.length / 2 - 1; i >= 0; i--) {
adjustHeap(arr,i,arr.length);
}
//将堆顶元素与末尾元素交换。将最大的元素沉到数组末端
for (int j = arr.length-1; j > 0; j--) {
swap(arr, 0, j);
//重新调整结构,使其满足堆定义,然后继续交换堆顶元素与当前末尾元素,反复执行调整+交换步骤,直到整个序列有序
adjustHeap(arr,0,j);
}
}
/** 将一个数组(二叉树)调整成一个大顶堆
* 功能:完成将以i对应的非叶子节点的树调整成大顶堆
* @param arr 待调整的数组
* @param i 表示非叶子节点在数组中的索引
* @param length 表示对多少个元素继续调整,length是在逐渐的减少
*/
public void adjustHeap(int[] arr,int i, int length) {
int temp = arr[i]; //取出当前元素的值保存在临时变量
//k = 2 * i + 1是i节点的左子节点
for (int k = 2 * i + 1; k < length; k = 2 * k + 1) {
if(k + 1 < length && arr[k] < arr[k + 1]) { //说明左子节点的值小于右子节点的值
k++; //k指向右子节点
}
if(arr[k] > temp) { //如果子节点大于父节点
arr[i] = arr[k]; //把较大的值赋给当前节点
i = k; //i指向k继续比较
} else {
break;
}
}
//当for循环结束后,我们已经将以i为父节点的树的最大值,放在了最顶(局部)
arr[i] = temp; //将temp的值放到调整后的位置
}
private void swap(int[] arr, int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
- 计数排序(Counting Sort)
计数排序是一种非比较排序算法,它的基本思想是统计数组中每个元素出现的次数,然后根据元素出现的次数依次将元素放入有序的数组中。
计数排序时间复杂度为O(n+k),其中k为待排序的元素的最大值。
import org.junit.jupiter.api.Assertions;
import java.util.Arrays;
public class Counting {
public static void countingSort(int[] arr) {
int n = arr.length;
// 取出数组中最大值
int max = getMax(arr);
int[] count = new int[max + 1];
// 统计每个元素出现的次数
for (int i = 0; i < n; i++) {
count[arr[i]]++;
}
// 计算每个元素在有序序列中的位置
for (int i = 1; i <= max; i++) {
// 因为count包含了每个数据出现的次数,所以从小到大,
// 逐个往前加得到就是原数组中每个元素在有序序列中应有的位置
count[i] += count[i - 1];
}
// 输出有序序列
int[] sortedArr = new int[n];
for (int i = n - 1; i >= 0; i--) {
int item = arr[i];//元素
int itemPos = count[item];// 元素在有序数组中的位置
sortedArr[itemPos - 1] = item; // 将元素填入有序数组
count[item]--;
}
// 将有序序列复制回原数组
System.arraycopy(sortedArr, 0, arr, 0, n);
}
private static int getMax(int[] arr) {
int max = arr[0];
for (int i = 1; i < arr.length; i++) {
if (arr[i] > max) {
max = arr[i];
}
}
return max;
}
public static void main(String[] args) {
int[] arr = {5, 2, 6, 8, 3, 1, 6, 5, 12};
int[] expectedArr = {1, 2, 3, 5, 5, 6, 6, 8, 12};
Counting.countingSort(arr);
System.out.println("arr = " + Arrays.toString(arr));
Assertions.assertArrayEquals(expectedArr, arr);
}
}
- 桶排序(Bucket Sort)
桶排序是一种非比较排序算法,它的基本思想是将待排序的数组分到有限数量的桶里,然后对每个桶进行排序,最后依次将所有桶中的元素取出来,组成有序的数组。
桶排序的时间复杂度为O(n),其中n为待排序元素的个数。
import org.junit.jupiter.api.Assertions;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
public class Bucket {
public static void main(String[] args) {
int[] arr = {5, 2, 8, 3, 12, 35, 57, 1, 6};
int[] expectedArr = {1, 2, 3, 5, 6, 8, 12, 35, 57};
Bucket.bucketSort(arr, 20);
System.out.println("arr = " + Arrays.toString(arr));
Assertions.assertArrayEquals(expectedArr, arr);
}
/**
* 桶排序
* @param arr 待排序数组
* @param bucketSize 桶大小,数据不宜过大,桶越大,后续对桶内数据排序越耗时
*/
public static void bucketSort(int[] arr, int bucketSize) {
if (arr.length == 0) {
return;
}
// 循环数组,先找到最小值和最大值
int minValue = arr[0];
int maxValue = arr[0];
for (int i = 1; i < arr.length; i++) {
if (arr[i] < minValue) {
minValue = arr[i];
} else if (arr[i] > maxValue) {
maxValue = arr[i];
}
}
// 根据桶的大小,计算桶个数,并初始化桶
int bucketCount = (maxValue - minValue) / bucketSize + 1;
List<List<Integer>> buckets = new ArrayList<>(bucketCount);
for (int i = 0; i < bucketCount; i++) {
buckets.add(new ArrayList<>());
}
for (int i = 0; i < arr.length; i++) {
int bucketIndex = (arr[i] - minValue) / bucketSize;
buckets.get(bucketIndex).add(arr[i]);
}
int currentIndex = 0;
for (int i = 0; i < bucketCount; i++) {
List<Integer> bucket = buckets.get(i);
// 对桶内数据进行排序
Collections.sort(bucket);
// 将数据逐个从桶内取出,并存入数组
for (int j = 0; j < bucket.size(); j++) {
arr[currentIndex++] = bucket.get(j);
}
}
}
}
- 基数排序(Radix Sort)
基数排序是一种非比较排序算法,它的基本思想是将待排序的数组按照位数(个位、十位、百位)进行划分,然后依次对每个位上的数字进行排序,最终得到有序的数组。
基数排序的时间复杂度为O(d(n+k)),其中d为最大元素的位数,n为待排序元素的个数,k为桶的个数。
public class Radix {
public static void radixSort(int[] arr) {
if (arr.length == 0) {
return;
}
// 循环取得数组中的最大值
int maxNum = arr[0];
for (int i = 1; i < arr.length; i++) {
if (arr[i] > maxNum) {
maxNum = arr[i];
}
}
// 根据最大值算出数组中的最大位数,个位、十位、百位、千位等
int maxDigit = 0;
while (maxNum != 0) {
maxNum /= 10;
maxDigit++;
}
// 初始化10个list,分别存放位数是0-9的10组数字
List<List<Integer>> buckets = new ArrayList<>(10);
for (int i = 0; i < 10; i++) {
buckets.add(new ArrayList<>());
}
int mod = 10; // 初始10,用于数据个位数取模
int div = 1; // 桶序号除数
// 按位数循环数组,个位循环1次,十位循环2次,百位循环3次,以此类推!
for (int i = 0; i < maxDigit; i++, mod *= 10, div *= 10) {
// 循环数组,将数据分别存入桶中
// 第一次循环,桶里面的个位数顺序排序完成
// 第二次循环,个位、十位都排序完成
// 第三次循环,个位、十位、百位都排序完成
for (int j = 0; j < arr.length; j++) {
// 计算当前位数的桶序号
int bucketIndex = (arr[j] % mod) / div;
buckets.get(bucketIndex).add(arr[j]);
}
// 循环桶列表,将当前位数已排序的数据放入数组中
int currentIndex = 0;
for (int j = 0; j < 10; j++) {
List<Integer> bucket = buckets.get(j);
for (int k = 0; k < bucket.size(); k++) {
arr[currentIndex++] = bucket.get(k);
}
bucket.clear();
}
}
}
public static void main(String[] args) {
int[] arr = {5, 2, 8, 3, 12, 35, 57, 1, 6};
int[] expectedArr = {1, 2, 3, 5, 6, 8, 12, 35, 57};
Radix.radixSort(arr);
System.out.println("arr = " + Arrays.toString(arr));
Assertions.assertArrayEquals(expectedArr, arr);
}
}