算法复杂度的简要介绍
在时间复杂度的表达式中,只要高阶项,不要低阶项,也不要高阶项的系数,剩余的部分假设为,那么该算法的时间复杂度为。空间复杂度同理。
选择排序
复杂度
时间复杂度,空间复杂度。
核心思想
遍历寻找最小(或最大),然后进行交换。
//排序结果为一个从小到大的数组
public static void selectionSort(int[] arr){
//base case
if(arr == null || arr.length < 2){
return;
}
for(int = 0; i < arr.length - 1; i++){
int minIndex = i; //初始化 minIndex,即当前数组中最小数所在位置
for(j = i + 1; j < arr.length; j++){
minIndex = arr[j] < arr[minIndex] ? j : minIndex;//将 minIndex定位到 i及 i之后的所有剩余元素中最小的一个元素的所在位置
}
swap(arr, i, minIndex);// i从0开始依次走过数组的每个位置,每走一次就将当前剩余数组元素中能找到的最小元素交换至当前i位置
}
}
//完成两数位置交换
public static void swap(int[] arr, int i, int j){
arr[i] = arr[i] ^ arr[j];
arr[j] = arr[i] ^ arr[j];
arr[i] = arr[i] ^ arr[j];
}
//或者使用以下写法,使用一个额外空间
public static void swap(int[] arr, int i, int j){
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
冒泡排序
复杂度
时间复杂度,空间复杂度。
核心思想
遍历数组,将当前遍历到的位置的元素与他下一位的元素比较大小,将更大(或更小)的数放在下一位的位置上。
//排序结果为一个从小到大的数组
public ststic void bubbleSort(int[] arr){
//base case
if(arr == null || arr.length < 2){
return;
}
for(int i = arr.length - 1; i > 0; i--){
for(int j = 0; j < i; j++){
if(arr[j] > arr[j+1]){
swap(arr, j, j+1);
}
}
}
}
插入排序
复杂度
时间复杂度,空间复杂度。
核心思想
遍历中的每一趟将一个待排序元素,按其大小插入到前面已经排好序的一组元素的适当位置上,直到所有待排序元素元素全部插入为止。
public static void insertionSort(int[] arr){
//base case
if(arr == null || arr.length < 2){
return;
}
for(int i = 1; i < arr.length; i++){// 0 ~ i 做到有序
for(int j = i - 1; j >= 0 && arr[j] > arr[j+1]; j--){
swap(arr, j, j+1);
}
}
}
归并排序
归并排序(Merge sort)是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。
复杂度
时间复杂度,空间复杂度。
动图演示
迭代法实现归并排序的动图演示
实现原理
归并排序的实现有两种方法:
- 自上而下的递归(所有递归的方法都可以用迭代重写,所以就有了第2种方法);
- 将序列每相邻两个数字进行归并操作,形成个序列,排序后每个序列包含两个元素;
- 将上述序列再次归并,形成个序列,每个序列包含四个元素;
- 重复归并步骤,直到所有元素排序完毕。
- 自下而上的迭代。
- 申请空间用于存放归并后的序列,其大小为两个已排序的序列之和;
- 设定两个指针,其初始位置分别为两个已排序的序列的起始位置;
- 比较两个指针所指向的元素,选择相对小的元素放入到归并空间,并移动指针到下一位置;
- 重复步骤3直到某一指针到达序列尾;
- 将另一序列剩下的所有元素直接复制到归并序列尾。
分治模式在每一层递归上有三个步骤:
- 分解(Divide):将个元素分成个含个元素的子序列。
- 解决(Conquer):用归并排序法对两个子序列进行递归地排序。
- 合并(Combine):归并两个已排序的子序列,得到排序结果。
//最开始初始化时 L = 0, R = arr.length
public static void process(int[] arr, int L, int R){
//base case
if(L == R){
return;
}
//这样求中间值可以防止溢出;使用位运算可以更快,左移一位相当于除以2
int mid = L + ((R - L) >> 1);
//递归处理部分
process(arr, L, mid);
process(arr, mid+1, R);
mergeSort(arr, L, mid, R);
}
//迭代法实现归并排序,排序结果从小到大
public static void mergeSort(int[] arr, int L, int mid, int R){
//申请空间用于存放归并后的序列,其大小为两个已排序的序列之和
int[] help = new int[R - L + 1];
//设定两个指针,其初始位置分别为两个已排序的序列的起始位置
int p1 = L;
int p2 = mid + 1;
int i = 0;
//比较两个指针所指向的元素,选择相对小的元素放入到归并空间,并移动指针到下一位置。重复此步骤直到某一指针到达序列尾
while(p1 <= mid && p2 <= R){
help[i++] = arr[p1] <= arr[p2] ? arr[p1++] : arr[p2++];
}
//将另一序列剩下的所有元素直接复制到归并序列尾
while(p1 <= mid){
help[i++] = arr[p1++];
}
while(p2 <= R){
help[i++] = arr[p2++];
}
for(i = 0; i < arr.length; i++){
arr[L + i] = help[i];
}
}
快速排序
复杂度
动图演示
快速排序的动图演示
实现原理
快速排序(Quicksort)是对冒泡排序算法的一种改进,也是一种采用分治法(Divide and Conquer)的排序算法。它选择一个元素作为枢轴元素(pivot),并围绕选定的枢轴元素对给定数组进行分区(partition)。快速排序实现步骤如下:
- 首先设定一个分界值,通过该分界值将数组分成左右两部分。
- 将大于(或等于)分界值的数据集中到数组右边,小于(或等于)分界值的数据集中到数组的左边。此时,左边部分中各元素都小于或等于分界值,而右边部分中各元素都大于或等于分界值。(等于分界值的数值挑其中一边放入即可)
- 然后左边和右边的数据可以独立排序。对于左侧的数组数据,又可以取一个分界值,将该部分数据分成左右两部分,同样在左边放置较小值,右边放置较大值。右侧的数组数据也可以做类似处理。
- 重复上述过程,可以看出,这是一个递归定义。通过递归将左侧部分排好序后,再递归排好右侧部分的顺序。当左、右两个部分各数据排序完成后,整个数组的排序也就完成了。
快速排序有很多不同的版本,它们以不同的方式选择枢轴元素(分界值):
- 总是选择第一个元素作为枢轴元素。
- 总是选择最后一个元素作为枢轴元素。
- 选择中值作为枢轴元素。
- 随机选一个元素作为枢轴元素。
快排1.0:选择最后一个元素的数为基准分开数组,时间复杂度。
快排2.0:在1.0的基础上将等于分界值的元素单独分区,时间复杂度。
快排3.0:随机抽取数组中一个数放在数组的最后,然后进行分区及后续操作,时间复杂度。
快排3.0的Java实现代码如下:
//初始化时 lowIndex = 0, highIndex = arr.length - 1
public static void quickSort(int[] arr, int lowIndex, int highIndex){
//base case
if(arr == null || arr.length < 2){
return;
}
if(lowIndex > highIndex){
return;
}
// 随机选择pivot
int pivotIndex = new Random(),nextInt(highIndex - lowIndex) + lowIndex;
int pivot = arr[pivotIndex];
swap(arr, pivotIndex, highIndex);
// 处理arr[lowIndex~highIndex]的部分
int leftPointer = lowIndex; // 小于等于区
int rightPointer = highIndex; // 大于等于区
while(lowIndex < highIndex){
while(leftPointer <= pivot && leftPointer < rightPointer){
leftPointer++;
}
while(rightPointer >= pivot && leftPointer < rightPointer){
rightPointer--;
}
swap(arr, leftPointer, rightPointer);
}
swap(arr, leftPointer, highIndex);
// 递归处理子数组
quickSort(arr, lowIndex, leftPointer - 1);
quickSort(arr, leftPointer + 1, highIndex);
}
堆排序
堆的定义
- 堆结构就是用数组实现的完全二叉树结构。
- 完全二叉树中如果每棵子树的最大值都在顶部就是大根堆。
- 完全二叉树中如果每棵子树的最小值都在顶部就是小根堆。
一个数组[3,5,2,7,1,9,6]
其下标为[0,1,2,3,4,5,6]
画成完全二叉树就是
用下标来表示的完全二叉树就是
那么,下标位置的左孩子为:,右孩子为:,父结点为:,定义的父结点还是,左右孩子的关系为。
堆结构的 heapInsert 与 heapify 操作
heapInsert: 每次输入一个数,都和父结点比较以形成大(或小)根堆。即向堆中插入新元素并形成新的堆的过程,是一个向上调整的过程。
//以大根堆为例
public static void heapInsert(int[] arr, int index){
while(arr[index] > arr[(index - 1) / 2]){
swap(arr, index, (index - 1) / 2);
index = (index - 1) / 2;
}
}
heapify: 返回最大的数arr[0](假设是大根堆),并将剩余的数排成大根堆。将arr[0]与数组的最后一个数交换位置,通过heapSize--的操作去掉最后一个数,并重复父结点与左右子结点的比较过程,形成新的大根堆。是一个向下调整的重新堆化的过程。
public static void heapify(int[] arr, int index, int heapSize){
int left = index * 2 + 1;//左孩子的下标
while(left < heapSize){
//两个孩子中谁的值较大,就把其下标给largest
int largest = ((left + 1 < heapSize) && (arr[left + 1] > arr[left])) ? left + 1 : left;
//父和较大孩子之间谁的值较大,就把其下标给largest
largest = arr[largest] > arr[index] ? largest : index;
if(largest == index){
break;
}
swap(arr, largest, index);
index = largest;
left = index * 2 + 1;
}
}
堆排序
堆排序的Java实现如下:
public static void heapSort(int[] arr){
//base case
if(arr == null || arr.length < 2){
return;
}
for(int i = 0; i < arr.length; i++){
heapInsert(arr, i);
}
int heapSize = arr.length;
swap(arr, 0, --heapSize);
while(heapSize > 0){
heapify(arr, 0, heapSize);
swap(arr, 0, --heapSize);
}
}
优先级队列结构就是堆结构
Java中小根堆的实现就是PriorityQueue默认构造。
public static void main(){
PriorityQueue<Integer> heap = new PriorityQueue<>();
//add添加元素
heap.add(8);
heap.add(4);
while(!heap.isEmpty()){//isEmpty判断是否为空
System.out.println(heap.poll());//poll弹出并移除元素
}
}
桶排序
桶排序(Bucket Sort)又称箱排序,是一种不基于比较的排序,而是利用“桶”来完成排序的算法。
复杂度
时间复杂度。
核心思想
- 根据要排序的数据样本量,设置一定数量的、有序的空桶。
- 例如要对一组取值范围在的数据进行排序,可以设置十个桶,其存放数据的范围分别对应。
- 遍历输入数据,并且把数据一个一个放到对应的桶里去。
- 对每个不是空的桶内部进行排序(可以使用别的排序算法,也可以使用桶排序进行递归操作)。
- 从不是空的桶里把排好序的数据拼接起来。
桶排序思想下的排序
计数排序
复杂度
时间复杂度,空间复杂度
核心思想
- 找出待排序的数组中最大和最小的元素,分别记为和;
- 新建一个空数组,其长度为,其每项元素初始化为;
- 使用作为基准偏移量,对中出现的所有的元素分别计数累加至中;
- 例如,中有一个元素,则对应
- 输出结果时,按照下标从小到大,依次加上基准偏移量的值,然后输出。
实现代码
基数排序
复杂度
时间复杂度,空间复杂度
核心思想
基数排序是按照低位先排序,然后收集;再按照高位排序,然后再收集;依次类推,直到最高位。有时候有些属性是有优先级顺序的,先按低优先级排序,再按高优先级排序。最后的次序就是高优先级高的在前,高优先级相同的低优先级高的在前。
实现代码
public class RadixSort {
public static void main(String[] args) {
int[] array = {72,538,627,56,332,453,312,4};
radixSort(array);
}
/**
* 基数排序
*/
public static void radixSort(int[] array){
//初始化桶
int[][] bucket = new int[10][array.length];
//初始化一个数组用来存放指向每个桶中最大的元素的指针
int[] bucketIndex = new int[10];
//获取数组中最大的元素
int max = 0;
for (int i = 0; i < array.length; i++) {
if (array[i] > max){
max = array[i];
}
}
//获取最大元素的长度
int maxLength = (max+"").length();
//将数组中的元素按照指定规则放入到桶中
for (int i = 0; i < maxLength; i++) {
int div = (int)Math.pow(10,i);
for (int j = 0; j < array.length; j++) {
//获取元素的个位、十位、百位、千位...
int element = (array[j]/div)%10;
bucket[element][bucketIndex[element]] = array[j];
bucketIndex[element]++;
}
int index = 0;
for (int k = 0; k < bucketIndex.length; k++) {
//桶中有元素
if (bucketIndex[k] != 0){
//取出桶中元素
for (int j = 0; j < bucketIndex[k]; j++) {
array[index++] = bucket[k][j];
}
}
bucketIndex[k] = 0;
}
System.out.println("第" + (i+1) + "次排序的结果为" + Arrays.toString(array));
}
}
}
排序算法的稳定性及其汇总
同样值的元素之间,如果不因为排序而改变其相对次序,就说这个排序是有稳定性的,否则就没有。
不具备稳定性的排序:选择排序、快速排序、堆排序...
具备稳定性的排序:冒泡排序、归并排序、一切桶排序思想下的排序...
| 时间复杂度 | 空间复杂度 | 稳定性 | |
|---|---|---|---|
| 选择排序 | 不具有❌ | ||
| 冒泡排序 | 具有✅ | ||
| 插入排序 | 具有✅ | ||
| 归并排序 | 具有✅ | ||
| 快速排序 | 不具有❌ | ||
| 堆排序 | 不具有❌ |
目前没有找到时间复杂度,额外空间复杂度,而又稳定的排序。
实际应用中,在面对大样本量的数据时选用时间复杂度为的排序算法加快排序速度,面对小样本量的数据时选用时间复杂度为的排序算法,此时时间消耗相差无几,而时间复杂度为的排序算法通常更节省空间且更易于实现和理解。
Java自带的排序接口 Arrays.sort() 实现原理:如果是基础类型数据的排序则会使用快速排序完成;如果是非基础类型数据(自定类型数据)的排序会出于稳定性的考虑而使用归并排序来完成。