目录
1、冒泡排序
2、选择排序
3、插入排序
4、 希尔排序
5、快速排序
6、归并排序
7、堆排序
8、基数排序
9、算法总结
1、冒泡排序
算法思想:
以升序为例,冒泡排序的核心思想如下:
- Step1:从第一个元素开始,比较相邻的两个元素,如果前一个元素的值比后一个元素的值大,那么就进行交换。
- Step2:每组相邻元素,都是同样的操作,直到没有相邻元素可以比较为止,那么此时最后的元素就是最大的数。
- Step3:对剩余元素重复以上步骤,直到没有任何一对元素需要比较为止。
算法代码:
public static int[] bobbleSort(int[] arr){
//控制外层的循环次数
for (int i = 0; i < arr.length - 1; i++) {
//进行数组元素的比较
for (int j = 0; j < arr.length - 1 - i; j++) {
//如果前一个元素的值大于后一个元素的值,就进行交换
if (arr[j] > arr[j + 1]) {
int temp = arr[j + 1];
arr[j + 1] = arr[j];
arr[j] = temp;
}
}
}
return arr;
}
2、选择排序
算法思想:
选择排序首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。
算法代码:
public static int[] selectSort(int[] arr){
for (int i = 0; i < arr.length-1; i++) {
//标记最小下标
int min = i;
for (int j = i+1; j < arr.length; j++) {
//进行比较
if(arr[min] > arr[j]){
min = j;
}
}
//交换
if(i != min){
int temp = arr[min];
arr[min] = arr[i];
arr[i] = temp;
}
}
return arr;
}
3、插入排序
算法思想:
插入排序会选定一个下标,认为在这个下标之前的元素都是有序的。将下标所在的元素插入到其之前的序列中。接着再选取这个下标的后一个元素,继续重复操作。直到最后一个元素完成插入为止。一般从序列的第二个元素开始操作。
算法代码:
public static int[] insertSort(int[] arr){
//遍历所有数字
for(int i=1;i<arr.length;i++) {
//当前数字比它的前一个数字小
if(arr[i] < arr[i - 1]) {
int j;
// 把当前遍历的数字保存起来
int temp = arr[i];
for(j = i - 1; j >= 0 && arr[j] > temp; j--) {
// 前一个数字赋给后一个数字
arr[j + 1] = arr[j];
}
// 把临时变量赋给不满足条件的后一个元素
arr[j + 1] = temp;
}
}
return arr;
}
4、 希尔排序
算法思想:
某些情况下直接插入排序的效率极低。比如一个已经有序的升序数组,这时再插入一个比最小值还要小的数,也就意味着被插入的数要和数组所有元素比较一次。我们需要对直接插入排序进行改进。
希尔排序把序列按下标的一定增量(步长)分组,对每组分别使用插入排序。随着增量(步长)减少,一直到一,算法结束,整个序列变为有序。因此希尔排序又称缩小增量排序。一般来说,初次取序列的一半为增量,以后每次减半,直到增量为一。
算法代码:
public static int[] shellSort(int[] arr){
for(int step = arr.length/2; step > 0; step /= 2){
for (int i = 0; i < arr.length; i++) {
int temp = arr[i];
int j = i - step;
while(j >= 0 && temp < arr[j]){
arr[j+step] = arr[j];
j -= step;
}
arr[j+step] = temp;
}
}
return arr;
}
5、快速排序
算法思想:
快速排序将待排序的数组拆分成左右两个区间,然后选择一个基准值,基准值可以选择数组最左边、中间或者最右边的值,也可以随机选取。然后使用双指针left和right分别指向数组的起始位置和末尾位置,并与基准值进行比较,使得数组左区间的值都小于基准值,数组右区间的值都大于基准值。然后继续拆分,执行相同操作,直到每个区间只有一个数为止。举例如下:
算法代码:
public static int[] sort(int[] arr){
quickSort(arr, 0, arr.length-1);
return arr;
}
public static void quickSort(int[] array, int start, int end){
if(start >= end){
return;
}
//以最左边节点作为参考值
int pivot = array[start];
//定义两个指针
int left = start, right = end;
while(left < right){
//找到第一个比基准值小的值
while(left < right && array[right] >= pivot){
right --;
}
//把左边的数替换成右边的数
array[left] = array[right];
//找到第一个比基准值大的值
while(left < right && array[left] <= pivot){
left ++;
}
//把右边的数替换成左边的数
array[right] = array[left];
}
//把标准值赋给下标重合的位置
array[left] = pivot;
//处理左边
quickSort(array, start, left);
//处理右边
quickSort(array, left+1, end);
}
6、归并排序
算法思想:
归并排序是建立在归并操作上的一种有效,稳定的排序算法。该算法采用分治法的思想,是一个非常典型的应用。归并排序的思路如下:
- Step 1:将 n 个元素分成两个各含 n/2 个元素的子序列
- Step 2:借助递归,两个子序列分别继续进行第一步操作,直到不可再分为止
- Step 3:此时每一层递归都有两个子序列,再将其合并,作为一个有序的子序列返回上一层,再继续合并,全部完成之后得到的就是一个有序的序列
关键在于两个子序列应该如何合并。假设两个子序列各自都是有序的,那么合并步骤就是:
- Step 1:创建一个用于存放结果的临时数组,其长度是两个子序列合并后的长度
- Step 2:设定两个指针,最初位置分别为两个已经排序序列的起始位置
- Step 3:比较两个指针所指向的元素,选择相对小的元素放入临时数组,并移动指针到下一位置
- Step 4:重复步骤 3 直到某一指针达到序列尾
- Step 5:将另一序列剩下的所有元素直接复制到合并序列尾
算法代码:
public static int[] mergeSort(int[] arr, int low, int high){
//获取中间值
int mid = low + (high - low) / 2;
if(low < high){
//处理左边数组
mergeSort(arr, low, mid);
//处理右边数组
mergeSort(arr, mid+1, high);
merge(arr, low, mid, high);
}
return arr;
}
public static void merge(int[] arr, int low, int mid, int high){
int[] tempArray = new int[high-low+1];
//记录第一个数组中需要遍历的下标
int i = low;
//记录第二个数组中需要的遍历的下标
int j = mid + 1;
//记录临时数组的下标
int index = 0;
//遍历两个数组,进行比较,将较小的数字放入临时数组
while(i <= mid && j <= high){
if(arr[i] <= arr[j]){
tempArray[index] = arr[i];
i++;
}else{
tempArray[index] = arr[j];
j++;
}
index ++;
}
//将剩余数据放入临时数组
while(i <= mid){
tempArray[index++] = arr[i++];
}
while(j <= high){
tempArray[index++] = arr[j++];
}
//把临时数组中的数据放入原数组
for (int k = 0; k < tempArray.length; k++) {
arr[k+low] = tempArray[k];
}
}
7、堆排序
算法思想:
对于任何一个数组都可以看成一颗完全二叉树。堆是具有以下性质的完全二叉树:每个结点的值都大于或等于其左右孩子结点的值,称为大顶堆;或者每个结点的值都小于或等于其左右孩子结点的值,称为小顶堆。如下图:
像上图的大顶堆,映射为数组,就是 [50, 45, 40, 20, 25, 35, 30, 10, 15]。可以发现第一个下标的元素就是最大值,将其与末尾元素交换,则末尾元素就是最大值。所以堆排序的思想可以归纳为以下两步:
- Step 1:根据初始数组构造堆
- Step 2:每次交换第一个和最后一个元素,然后将除最后一个元素以外的其他元素重新调整为大顶堆
重复以上两个步骤,直到没有元素可操作,就完成排序了。
我们需要把一个普通数组转换为大顶堆,调整的起始点是最后一个非叶子结点,然后从左至右,从下至上,继续调整其他非叶子结点,直到根结点为止。
算法代码:
public static void constructMaxHeap(int[] arr, int size, int index){
//左子节点
int leftNode = 2 * index + 1;
//右子节点
int rightNode = 2 * index + 2;
int parent = index;
//和父节点的两个孩子节点进行比较,找到最大的节点
if(leftNode < size && arr[leftNode] > arr[parent]){
parent = leftNode;
}
if(rightNode < size && arr[rightNode] > arr[parent]){
parent = rightNode;
}
//交换位置
if(parent != index){
int temp = arr[index];
arr[index] = arr[parent];
arr[parent] = temp;
//因为交换位置之后可能出现字数不满足大顶堆的条件,因此要对字数进行调整
constructMaxHeap(arr, size, parent);
}
}
public static int[] heapSort(int[] arr){
//起始位置是最有一个非叶子节点,即最后一个节点的父节点
int start = (arr.length-1) / 2;
//调整为最大堆
for (int i = start; i >= 0; i--) {
constructMaxHeap(arr, arr.length, i);
}
//先把数组中第0个位置的数和堆中最后一个数字交换位置,再把前面的处理为大顶堆
for (int i = arr.length-1; i > 0; i--) {
int temp = arr[0];
arr[0] = arr[i];
arr[i] = temp;
constructMaxHeap(arr, i, 0);
}
return arr;
}
8、基数排序
算法思想:
基数排序的原理是将整数按位数切割成不同的数字,然后按每个位数分别比较。为此需要将所有待比较的数值统一为同样的数位长度,数位不足的数在高位补零。
算法代码:
/**
* 基数排序
*/
public static void radixSort(int[] arr) {
// 存放数组中的最大数字
int max = Integer.MIN_VALUE;
for (int value : arr) {
if (value > max) {
max = value;
}
}
// 计算最大数字是几位数
int maxLength = (max + "").length();
// 用于临时存储数据
int[][] temp = new int[10][arr.length];
// 用于记录在 temp 中相应的下标存放数字的数量
int[] counts = new int[10];
// 根据最大长度的数决定比较次数
for (int i = 0, n = 1; i < maxLength; i++, n *= 10) {
// 每一个数字分别计算余数
for (int j = 0; j < arr.length; j++) {
// 计算余数
int remainder = arr[j] / n % 10;
// 把当前遍历的数据放到指定的数组中
temp[remainder][counts[remainder]] = arr[j];
// 记录数量
counts[remainder]++;
}
// 记录取的元素需要放的位置
int index = 0;
// 把数字取出来
for (int k = 0; k < counts.length; k++) {
// 记录数量的数组中当前余数记录的数量不为 0
if (counts[k] != 0) {
// 循环取出元素
for (int l = 0; l < counts[k]; l++) {
arr[index] = temp[k][l];
// 记录下一个位置
index++;
}
// 把数量置空
counts[k] = 0;
}
}
}
}
9、算法总结
排序法 | 最好情形 | 平均时间 | 最差情形 | 稳定度 | 空间复杂度 |
冒泡排序 | O(n) | O(n^2) | O(n^2) | 稳定 | O(1) |
快速排序 | O(nlogn) | O(nlogn) | O(n^2) | 不稳定 | O(nlogn) |
直接插入排序 | O(n) | O(n^2) | O(n^2) | 稳定 | O(1) |
希尔排序 | O(n) | O(nlogn) | O(nlogn) | 不稳定 | O(1) |
直接选择排序 | O(n^2) | O(n^2) | O(n^2) | 不稳定 | O(1) |
堆排序 | O(nlogn) | O(nlogn) | O(nlogn) | 不稳定 | O(nlogn) |
归并排序 | O(nlogn) | O(nlogn) | O(nlogn) | 稳定 | O(n) |