基本思想: 冒泡排序是排序算法中思想最简单的一种,通过多次比较数组中相邻的两个元素,将不符合顺序的元素进行两两交换,从而实现排序。
算法性能:最优时间复杂度O(n),最坏时间复杂度O(n2),平均时间复杂度O(n2)
算法稳定性:稳定
算法实现:
public int[] bubbuleSort(int[] nums){
if (nums == null || nums.length == 0) return null;
for (int i = 0; i < nums.length; i++){
for (int j = 0; j < nums.length-i-1; j++){
if (nums[j] > nums[j+1]) {
int t = nums[j];
nums[j] = nums[j+1];
nums[j+1] = t;
}
}
}
return nums;
}
算法思想:第一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,然后再从剩余的未排序元素中寻找到最小(大)元素,然后放到已排序的序列的末尾。以此类推,直到全部待排序的数据元素的个数为零。
算法性能:最坏、最优、平均时间复杂度均为O(n2)。
算法稳定性:不稳定
算法特点:与冒泡排序相比,选择排序每一趟先记录最小元素的下标,在一趟比较结束后才进行一次交换,每一次交换就会让一个元素落在正确的位置上,大幅度减少了数据交换的次数。但选择排序是不稳定的排序算法。
算法实现:
public int[] selectionSort(int[] nums){
if (nums == null || nums.length == 0) return null;
for (int i = 0; i < nums.length; i++){
int minIndex = i;
for (int j = i+1; j < nums.length; j++){
if (nums[j] < nums[minIndex]) minIndex = j;
}
if (minIndex != i){
int t = nums[i];
nums[i] = nums[minIndex];
nums[minIndex] = t;
}
}
return nums;
}
算法思想:插入排序的核心思想是将一个元素插入到已经排好序的有序数组中。假设数组中的第一个元素是有序的数组(唯一的元素可以认为是一个有序数组),将后面的元素依次插入到该有序数组中。
算法性能:理想情况下,如果待排序的数组本身是符合顺序的,则只需要比较n-1次即可。最坏情况下,待排序数组是逆序的,则需要做n(n-1)/2次比较,因此,算法的最坏时间复杂度为O(n2),平均时间复杂度为O(n2),最优时间复杂度O(n)。
算法稳定性:稳定
算法实现:
public int[] insertSort(int[] nums){
if (nums == null || nums.length == 0) return null;
for (int i = 1; i < nums.length; i++){
int temp = nums[i];
int j = i-1;
for (; j >= 0; j--){
if (temp < nums[j]) nums[j+1] = nums[j];
else {
break;
}
}
nums[j+1] = temp;
}
return nums;
}
算法思想:快速排序是分治和递归思想的典型应用。其思想是寻找一个基准数据,将大于基准的数据放在数组的右侧,小于基准的数据放在数组的左侧,然后左侧和右侧的两个数组分别进行递归的二次排序。简单来讲,快速排序就是找到基准数据在数组中的正确位置,每一趟排序实际是将一个基准数落位。
算法性能
平均时间复杂度:O(nlog n)。理想情况下,每次选择的元素恰好是大小位于中间的元素,每次将数组等分为两部分,分别排序。快速排序只需要经过log n趟划分,这样算法的平均时间复杂度为O(nlog n)
最坏时间复杂度:最坏时间复杂度O(n2)。最坏情况下,每次选择的基准数据都是数组中的最大或最小元素,这样分开的两个数组一个为空,另一个是原数组长度-1,因此快速排序需要经过n趟划分,其时间复杂度为O(n2).
算法稳定性:不稳定
算法实现:
//单向扫描的partition函数
private int partition(int[] nums, int left, int right){
int pivot = nums[right];
int i = left, j = left;
for (; j < right; j++){
if (nums[j] < pivot){
if (i != j){
int t = nums[i];
nums[i]= nums[j];
nums[j] = t;
}
i++;
}
}
int t = nums[i];
nums[i] = nums[j];
nums[j] = t;
return i;
}
public int[] quickSort(int[] nums, int left, int right){
if (nums == null || nums.length == 0) return null;
if (left > right) return nums;
int index = partition(nums, left, right);
if (index > left) quickSort(nums, left, index-1);
if (index < right) quickSort(nums,index+1,right);
return nums;
}
注意:快速排序中需要重点掌握的是partition函数,该函数有许多的实现版本,主要核心思想分为单向扫描和双向扫描两种方式,不同实现方式的算法性能稍有不同,本文给出demo的实现是单向扫描的方式。具体的分析大家可以参考大神的文章:https://www.cnblogs.com/sdlwlxf/p/5131793.html 。
算法思想:堆排序就是利用堆这种数据结构进行排序的一种算法。
堆结构的性质:堆是一棵完全二叉树,且该二叉树的根是是所有元素中最大(大顶堆)或最小(小顶堆)的元素。同时,二叉树的任何一棵子树也满足堆的性质,即任何一棵子树的跟也是该子树所有节点中最大(大顶堆)或最小(小顶堆)的元素。
算法流程:
时间复杂度:建堆的时间复杂度为O(n),调整一棵子树的堆结构的复杂度为O(log n),共调用了n-1次,其复杂度为O(nlog n)。因此堆排序的时间复杂度为O(nlog n)
算法稳定性:不稳定
算法实现:
public int[] heapSort(int[] nums){
// 根据输入的数组建立一个堆
for (int i = nums.length/2-1; i >= 0; i--){
heapfy(nums, i, nums.length);
}
for (int i = nums.length-1; i >= 0; i--){
//将堆顶元素交换至数组的末尾
int t = nums[i];
nums[i] = nums[0];
nums[0] = t;
// 将剩余元素重新调整为一个堆
heapfy(nums, 0, i);
}
return nums;
}
// 将以nums[i]为根的子树调整为一个大顶堆
private void heapfy(int nums[], int i, int heapSize){
int temp = nums[i];
for (int k = 2*i+1; k < heapSize; k++){
// 选择左右子节点中值较大的一个。
if (k+1 < heapSize && nums[k] < nums[k+1]) k++;
if (nums[k] > temp){
int t = nums[i];
nums[i] = nums[k];
nums[k] = t;
i = k;
}
else break;
}
}
注意:在实际应用中,一般不使用堆排序进行数据排序,而是用性能更好的快速排序。但是我们一般使用堆结构构建最大(大顶堆)或最小(小顶堆)优先队列,在进程调度等实际应用场景中,有比较广泛的应用。
算法思想:希尔排序是插入排序算法的一种改进,通过进行分组的插入排序,在初始阶段使数组部分有序,有效减少了数据对比和交换的次数,提高排序性能。它的基本思想是:对于n个待排序的数列,取一个小于n的整数gap(gap被称为步长)将待排序元素分成若干个组子序列,所有距离为gap的倍数的记录放在同一个组中;然后,对各组内的元素进行直接插入排序。 这一趟排序完成之后,每一个组的元素都是有序的。然后减小gap的值,并重复执行上述的分组和排序。重复这样的操作,当gap=1时,整个数列就是有序的。
希尔增量:选择初始的分组步长为gap=length/2,以后每趟排序缩小增量为gap=gap/2,这种递增序列是最常用的,称为希尔增量。但这并不是使希尔排序性能最优的序列,希尔排序的增量序列的选择与证明是个数学难题,
时间复杂度: 希尔排序的时间复杂度与步长(gap)的选取有密切关系,在最好情况下,时间复杂度为O(n),最坏情况下时间复杂度为O(nlog2 n),算法的渐进时间复杂度为O(nlog2 n)。
算法稳定性:不稳定
算法实现:
public int[] shellSort(int[] nums){
for (int gap = nums.length/2; gap > 0; gap /= 2){
for (int i = 0; i < gap; i++) //直接插入排序
{
for (int j = i + gap; j < nums.length; j += gap)
if (nums[j] < nums[j - gap])
{
int temp = nums[j];
int k = j - gap;
while (k >= 0 && nums[k] > temp)
{
nums[k + gap] = nums[k];
k -= gap;
}
nums[k + gap] = temp;
}
}
}
return nums;
}
算法思想:归并排序一般通过递归实现,是分治策略的典型应用。其根本思想是将两个分别有序的序列合并为一个有序序列。通过将一个待排序的数组从中间(middle = (left + right) / 2)一分为二,分别进行排序,然后将两个分别有序的数组合并为一个。通过不断划分,将每个数组划分为只有一个元素,并认为只有一个元素的数组为有序数组,然后不断进行合并。
时间复杂度:最好时间复杂度为O(n),最坏时间复杂度为:O(nlog n),平均时间复杂度为O(nlog n)。
算法稳定性:稳定
算法实现:
public int[] mergeSort(int[] nums, int left, int right){
if (left == right) return new int[] {
nums[left]};
int middle = (left + right) / 2;
int[] leftArray = mergeSort(nums,left,middle);
int[] rightArray = mergeSort(nums,middle+1,right);
int[] newArray = new int[leftArray.length+rightArray.length];
int i = 0, j = 0, k = 0;
// i为leftArray的操作下标,j为rightArray的操作下标,k为newArray的操作下标
while (i < leftArray.length && j < rightArray.length){
newArray[k++] = leftArray[i] < rightArray[j] ? leftArray[i++] : rightArray[j++];
}
while (i < leftArray.length) newArray[k++] = leftArray[i++];
while (j < rightArray.length) newArray[k++] = rightArray[j++];
return newArray;
}
算法思想:计数排序,即统计每个元素出现的次数,以此达到排序的效果。通过开辟另一个数组,从小到大记录每个元素出现的频次,并通过遍历辅助数组输出有序的序列。因此,计数排序是一种以空间换取时间的思想,且只能对整数进行排序,不能对实数排序。
时间复杂度:最优、最坏、平均时间复杂度均为O(n+k)。
空间复杂度:O(n+k)
算法稳定性:稳定
算法实现:
public int[] countSort(int[] nums){
int max = 0, min = 0;
for(int i = 0; i < nums.length; i++){
if (nums[i] > max) max = nums[i];
if (nums[i] < min) min = nums[i];
}
int[] data = new int[max-min+1];
for(int i = 0; i < nums.length; i++){
data[nums[i]-min]++;
}
for (int i=0,j=0;i < data.length;i++){
for (int k = 1; k <= data[i]; k++){
nums[j++] = min+i;
}
}
return nums;
}
算法思想:桶排序算法,将待排序的元素划分为若干个区间,每个区间称为一个桶。将待排序的数据按照大小划分在每个桶中,再对每个桶中的元素单独进行排序,最后将所用桶中的数据按大小依次合并,形成有序的数组。
时间复杂度:设有N个待排序的数据,被分为M个桶,一般使用快速排序对每个桶中的数据进行单独排序,快速排序的时间复杂度为O(N*log N),则桶排序的时间复杂度为O(N∗(log(N/M)+1))。
空间复杂度:O(N+M)
算法稳定性:桶排序的稳定性与桶中排序算法的稳定性有关,一般桶中使用快速排序,则该算法为不稳定的排序算法。
算法实现:
public int[] bucketSort(int[] arr){
// 计算最大值与最小值
int max = Integer.MIN_VALUE;
int min = Integer.MAX_VALUE;
for(int i = 0; i < arr.length; i++){
max = Math.max(max, arr[i]);
min = Math.min(min, arr[i]);
}
// 计算桶的数量
int bucketNum = (max - min) / arr.length + 1;
ArrayList<ArrayList<Integer>> bucketArr = new ArrayList<>(bucketNum);
for(int i = 0; i < bucketNum; i++){
bucketArr.add(new ArrayList<Integer>());
}
// 将每个元素放入桶
for(int i = 0; i < arr.length; i++){
int num = (arr[i] - min) / (arr.length);
bucketArr.get(num).add(arr[i]);
}
// 对每个桶进行排序
for(int i = 0; i < bucketArr.size(); i++){
Collections.sort(bucketArr.get(i));
}
// 将桶中的元素赋值到原序列
int index = 0;
for(int i = 0; i < bucketArr.size(); i++){
for(int j = 0; j < bucketArr.get(i).size(); j++){
arr[index++] = bucketArr.get(i).get(j);
}
}
return arr;
}