目录
1、时间复杂度
常用搜索算法:
- 搜索一个好的哈希表会得到 O(1) 复杂度
- 搜索一个均衡的树会得到 O(log(n)) 复杂度
- 搜索一个阵列会得到 O(n) 复杂度
- 最好的排序算法具有 O(n*log(n)) 复杂度
- 糟糕的排序算法具有 O(n^2) 复杂度
2、排序算法
算法 | 时间复杂度 | 空间复杂度 | 稳定性 | ||
---|---|---|---|---|---|
最好 | 最差 | 平均 | |||
插入排序 | O(n)(优化后) | O(n²) | O(n²) | O(1) | 稳定 |
选择排序 | O(n²) | O(n²) | O(n²) | O(1) | 不稳定 |
冒泡排序 | O(n)(优化后) | O(n²) | O(n²) | O(1) | 稳定 |
希尔排序 | O(n^1.3) | O(n²) | 不确定 | O(1) | 不稳定 |
堆排序 | O(nlogn) | O(nlogn) | O(nlogn) | O(1) | 不稳定 |
快速排序 | O(nlogn) | O(n²) | O(nlogn) | 最好O(logn) 最差O(n) | 不稳定 |
计数排序 | O(n+k) n个0到k之间的整数 | O(n+k) | O(n+k) | O(k) | 稳定 |
桶排序 | O(n) | O(nlogn) | O(n+c) 其中c=n*(logn-logm) m为桶的个数 | O(n+m) m为桶的个数 | 稳定 |
归并排序 | O(nlogn) | O(nlogn) | O(nlogn) | O(n) | 稳定 |
基数排序 | O(d*(n+r)) d是位数,r是基数 n是比较的数目 | O(d*(n+r)) | O(d*(n+r)) | O(n+r) | 稳定 |
2.1、插入排序
基本逻辑是,把元素分为已排序的和未排序的。每次从未排序的元素取出第一个,与已排序的元素从尾到头逐一比较,找到插入点,将之后的元素都往后移一位,腾出位置给该元素。
public static void sort(int[] arr) {
int temp;
for (int i = 1; i < arr.length; i++) {
//待排元素小于有序序列的最后一个元素时,向前插入
if (arr[i] < arr[i - 1]) {
temp = arr[i];
for (int j = i; j >= 0; j--) {
if (j > 0 && arr[j - 1] > temp) {
arr[j] = arr[j - 1];
} else {
arr[j] = temp;
break;
}
}
}
}
}
2.2、选择排序
算法中只是交换节点的val值,时间复杂度O(n^2),空间复杂度O(1),每次选中最大/小进行插入
public static void sort(int[] arr) {
for (int i = 0; i < arr.length-1; i++) {
int maxIndex = i;
for (int j = i+1; j <arr.length; j++) {
if (arr[j] < arr[maxIndex]) {
maxIndex = j;
}
}
swap(arr, i, maxIndex);
}
}
private static void swap(int[] arr, int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
2.3、快速排序
算法只交换节点的val值,平均时间复杂度O(nlogn),不考虑递归栈空间的话空间复杂度是O(1)
其实思想是蛮简单的,就是通过第一遍的遍历(让left和right指针重合)来找到数组的切割点。
- 第一步:首先我们从数组的left位置取出该数(20)作为基准(base)参照物。
- 第二步:从数组的right位置向前找,一直找到比(base)小的数,如果找到,将此数赋给left位置(也就是将10赋给20),此时数组为:10,40,50,10,60,left和right指针分别为前后的10。
- 第三步:从数组的left位置向后找,一直找到比(base)大的数,如果找到,将此数赋给right的位置(也就是40赋给10),此时数组为:10,40,50,40,60,left和right指针分别为前后的40。
- 第四步:重复“第二,第三“步骤,直到left和right指针重合,最后将(base)插入到40的位置,此时数组值为: 10,20,50,40,60,至此完成一次排序。
- 第五步:此时20已经潜入到数组的内部,20的左侧一组数都比20小,20的右侧作为一组数都比20大,以20为切入点对左右两边数按照"第一,第二,第三,第四"步骤进行,最终快排大功告成。
#include <iostream>
using namespace std;
void QuickSort(int arr[], int l, int r) {
if (l < r) {
int i, j, base;
i = l;
j = r;
base = arr[i];
while (i < j) {
while (i < j && arr[j] > base)
j--; // 从右向左找第一个小于base的数
if (i < j)
arr[i++] = arr[j];
while (i < j && arr[i] < base)
i++; // 从左向右找第一个大于base的数
if (i < j)
arr[j--] = arr[i];
}
arr[i] = base;
QuickSort(arr, l, i - 1); // 递归调用 base 左边
QuickSort(arr, i + 1, r); // 递归调用 base 右边
}
}
int main(int argc, char *argv[]) {
int dataArr[] = {9, 2, 7, 5, 6, 4, 3, 8, 1};
QuickSort(dataArr, 0, 8);
for (int i = 0; i < 9; i++) {
cout << dataArr[i] << ",";
}
}
快排每次执行都能确定一个元素的最终的位置
快速排序算法在什么情况下效率最低:
- 数组已经是正序(same order)排过序的。
- 数组已经是倒序排过序的。
- 所有的元素都相同(1、2的特殊情况)
2.4、归并排序
时间复杂度O(nlogn),不考虑递归栈空间的话空间复杂度是O(n)
public static void main(String[] args) {
int[] arr = {9,8,7,6,5,4,3,2,1};
merginSort(arr);
for (int i = 0; i < arr.length; i++) {
System.out.print(arr[i]+",");
}
}
public static void merginSort(int[] arr) {
int[] temp = new int[arr.length];
separate(arr, 0, arr.length-1, temp);
}
public static void separate(int[] arr, int left, int right, int[] temp) {
if (left < right) {
int mid = (left + right) / 2;
separate(arr, left, mid, temp);
separate(arr, mid+1, right, temp);
merging(arr, left, mid, right, temp);
}
}
public static void merging(int[] arr, int left, int mid, int right, int[] temp) {
int i = left; 左序列指针
int j = mid + 1; 右序列指针
int t = 0; 临时数组指针
while (i <= mid && j <= right) {
if (arr[i] < arr[j]) {
temp[t++] = arr[i++];
} else {
temp[t++] = arr[j++];
}
}
将左边剩余元素填充进temp中
while (i <= mid) {
temp[t++] = arr[i++];
}
将右序列剩余元素填充进temp中
while (j <= right) {
temp[t++] = arr[j++];
}
t = 0;
将temp中的元素全部拷贝到原数组中
while (left <= right) {
arr[left++] = temp[t++];
}
}
2.5、冒泡排序
基本的逻辑是,取第一个元素与后一个比较,如果大于后者,就与后者互换位置,不大于,就保持位置不变。再拿第二个元素与后者比较,如果大于后者,就与后者互换位置。一轮比较之后,最大的元素就移动到末尾。相当于最大的就冒出来了。再进行第二轮,第三轮,直到排序完毕。
10000个数据,前面1000倒序,后面9000顺序
int[] arr = new int[10000];
// 向数组写入10000个数据 前1000倒序 , 后9000顺序。
for (int i = 0; i < 10000; i++) {
if (i <= 1000) {
arr[i] = 1000 - i;
} else {
arr[i] = i;
}
}
改前
public static void bubbleSort(int[] arr) {
for (int i = 0; i < arr.length-1; i++) {
for (int j = 0; j < arr.length-i-1; j++) {
if (arr[j] > arr[j+1]) {
swap(arr, j, j+1);
}
}
}
}
改进1:加入flag标志位。整一次冒泡没有交换,则停止
public static void bubbleSort1(int[] arr) {
for (int i = 0; i < arr.length-1; i++) {
int flag = 0;
for (int j = 0; j < arr.length-i-1; j++) {
if (arr[j] > arr[j+1]) {
swap(arr, j, j+1);
flag = j;
}
}
if (flag == 0) {
break;
}
}
}
改进2:除了加入flag标志位,再加入边界,记录每次冒泡最后一次交换的位置
public static void bubbleSort2(int[] arr) {
int noSwapOffset = arr.length;
for (int i = 0; i < arr.length-1; i++) {
int flag = 0;
for (int j = 0; j < noSwapOffset-1; j++) {
if (arr[j] > arr[j+1]) {
swap(arr, j, j+1);
flag = j;
}
}
if (flag == 0) {
break;
}
noSwapOffset = flag + 1;
}
}
2.6、堆排序
https://blog.csdn.net/u010452388/article/details/81283998
堆排序的时间复杂度O(nlogn),额外空间复杂度O(1),是一个不稳定性的排序。堆的结构可以分为大根堆和小根堆,是一个完全二叉树,而堆排序是根据堆的这种数据结构设计的一种排序,下面先来看看什么是大根堆和小根堆
应用:快速找到topN
大小根堆:
每个结点的值都大于其左孩子和右孩子结点的值,称之为大根堆
每个结点的值都小于其左孩子和右孩子结点的值,称之为小根堆
我们对上面的图中每个数都进行了标记,上面的结构映射成数组就变成了下面这个样子
还有一个基本概念:查找数组中某个数的父结点和左右孩子结点,比如已知索引为 i 的数,那么
- 父结点索引:(i-1)/2(这里计算机中的除以2,省略掉小数)
- 左孩子索引:2*i+1
- 右孩子索引:2*i+2
堆排序基本步骤:
- 首先将待排序的数组构造成一个大根堆,此时,整个数组的最大值就是堆结构的顶端
- 将顶端的数与末尾的数交换,此时,末尾的数为最大值,剩余待排序数组个数为n-1
- 将剩余的n-1个数再构造成大根堆,再将顶端数与n-1位置的数交换,如此反复执行,便能得到有序数组
构造堆:
将无序数组构造成一个大根堆(升序用大根堆,降序就用小根堆)假设存在以下数组
主要思路:第一次保证0~0位置大根堆结构(废话),第二次保证0~1位置大根堆结构,第三次保证0~2位置大根堆结构...直到保证0~n-1位置大根堆结构(每次新插入的数据都与其父结点进行比较,如果插入的数比父结点大,则与父结点交换,否则一直向上交换,直到小于等于父结点,或者来到了顶端)
插入6的时候,6大于他的父结点3,即arr(1)>arr(0),则交换;此时,保证了0~1位置是大根堆结构,如下图:
插入8的时候,8大于其父结点6,即arr(2)>arr(0),则交换;此时,保证了0~2位置是大根堆结构,如下图
插入5的时候,5大于其父结点3,则交换,交换之后,5又发现比8小,所以不交换;此时,保证了0~3位置大根堆结构,如下图
插入7的时候,7大于其父结点5,则交换,交换之后,7又发现比8小,所以不交换;此时整个数组已经是大根堆结构
固定最大值再构造堆:
此时,我们已经得到一个大根堆,下面将顶端的数与最后一位数交换,然后将剩余的数再构造成一个大根堆
此时最大数8已经来到末尾,则固定不动,后面只需要对顶端的数据进行操作即可,拿顶端的数与其左右孩子较大的数进行比较,如果顶端的数大于其左右孩子较大的数,则停止,如果顶端的数小于其左右孩子较大的数,则交换,然后继续与下面的孩子进行比较
下图中,5的左右孩子中,左孩子7比右孩子6大,则5与7进行比较,发现5<7,则交换;交换后,发现5已经大于他的左孩子,说明剩余的数已经构成大根堆,后面就是重复固定最大值,然后构造大根堆
如下图:顶端数7与末尾数3进行交换,固定好7,
剩余的数开始构造大根堆 ,然后顶端数与末尾数交换,固定最大值再构造大根堆,重复执行上面的操作,最终会得到有序数组
public static void main(String[] args) {
int[] arr = {5,2,6,7,9,8,1,3,4};
bigHeap(arr); 首先构造大根堆
int size = arr.length;
while (size > 1) {
swap(arr, 0, size-1); 首元素跟未排序的尾元素交换
size--;
heapSort(arr, 0, size); 重新构造大根堆
}
printArr(arr);
}
public static void bigHeap(int[] arr) {
for (int i = 0; i < arr.length; i++) {
int currentIndex = i;
int fatherIndex = (currentIndex - 1) / 2;
while (arr[currentIndex] > arr[fatherIndex]) {
swap(arr, currentIndex, fatherIndex);
currentIndex = fatherIndex;
fatherIndex = (currentIndex - 1) / 2;
}
}
}
public static void heapSort(int[] arr, int index, int size) {
int leftChild = 2 * index + 1;
int rightChild = 2 * index + 2;
while (leftChild < size) {
int largeIndex;
if (arr[leftChild] < arr[rightChild] && rightChild < size) {
largeIndex = rightChild;
} else {
largeIndex = leftChild;
}
if (arr[index] > arr[largeIndex]) {
break; 当前父节点比子节点都大的话直接退出
}
swap(arr, index, largeIndex);
index = largeIndex;
leftChild = 2 * index + 1;
rightChild = 2 * index + 2;
}
}
public static void swap(int[] arr, int l, int r) {
int temp = arr[l];
arr[l] = arr[r];
arr[r] = temp;
}
public static void printArr(int[] arr) {
for (int i = 0; i < arr.length; i++) {
System.out.print(arr[i]+", ");
}
}
2.7、希尔排序
希尔排序是把序列按一定间隔分组,对每组使用直接插入排序;随着间隔减小,一直到1,使得整个序列有序。(网上的例子大多是偶数个,不能全面的反应整个排序过程)分组公式为N = N/2(还有其他的变种公式)
开始排序:
第一次分组值为9/2=4,分组结果如下:
每组内部实现插入排序,因为前面4组只有两个元素,看似好想只是交换了顺序,实际是插入排序,每组排序结果如下:
第一次分组排序后
第二次分组值为4/2=2,分组结果如下:
每组内部实现插入排序,每组排序结果如下:
第二次分组排序后
类似还有第三次分组值为2/2=1,结果类似处理
public static void sort(int[] arr) {
int number = arr.length / 2;
int swapIndex;
int temp;
while (number >= 1) {
for (int i = number; i < arr.length; i++) {
temp = arr[i];
swapIndex = i - number;
while (swapIndex >= 0 && arr[swapIndex] > temp) {
arr[swapIndex + number] = arr[swapIndex];
swapIndex -= number;
}
arr[swapIndex + number] = temp;
}
number /= 2;
}
}
2.8、计数排序
计数排序的核心在于将输入的数据值转化为键存储在额外开辟的数组空间中。作为一种线性时间复杂度的排序,计数排序要求输入的数据必须是有确定范围的整数。
计数排序的特征:当输入的元素是 n 个 0 到 k 之间的整数时,它的运行时间是 Θ(n + k)。计数排序不是比较排序,排序的速度快于任何比较排序算法。
假设:数组里有20个随机数,取值范围为从0到10,要求用最快的速度把这20个整数从小到大进行排序。于是我们可以建立一个长度为11的数组,数组下标从0到10,元素初始值全为0,如下所示:
先假设20个随机整数的值是:9, 3, 5, 4, 9, 1, 2, 7, 8,1,3, 6, 5, 3, 4, 0, 10, 9, 7, 9。让我们先遍历这个无序的随机数组,每一个整数按照其值对号入座,对应数组下标的元素进行加1操作。比如第一个整数是9,那么数组下标为9的元素加1:
第二个整数是3,那么数组下标为3的元素加1:
继续遍历数列并修改数组,最终,数列遍历完毕时,数组的状态如下:
数组中的每一个值,代表了数列中对应整数的出现次数。有了这个统计结果,排序就很简单了,直接遍历数组,输出数组元素的下标值,元素的值是几,就输出几次:0, 1, 1, 2, 3, 3, 3, 4, 4, 5, 5, 6, 7, 7, 8, 9, 9, 9, 9, 10。显然,这个输出的数列已经是有序的了。
public static void sort(int[] arr) {
int[] countArr = {0,0,0,0,0,0,0,0,0,0,0};
for (int i = 0; i < arr.length; i++) {
countArr[arr[i]]++;
}
int count = 0;
for (int i = 0; i < countArr.length; i++) {
while (countArr[i]-- > 0 ) {
arr[count++] = i;
}
}
}
优化:对于计数数组,可以根据数据的实际值来计算,比如数据的范围是90~100,那么计数数组可以是arr[11],排序前可以先计算出数据的最大值和最小值,根据最大最小的差值来定义计数数组的大小。
2.9、桶排序
桶排序是计数排序的升级版。它利用了函数的映射关系,高效与否的关键就在于这个映射函数的确定。为了使桶排序更加高效,我们需要做到这两点:
- 在额外空间充足的情况下,尽量增大桶的数量
- 使用的映射函数能够将输入的 N 个数据均匀的分配到 K 个桶中
同时,对于桶中元素的排序,选择何种比较排序算法对于性能的影响至关重要。
public class BucketSort implements IArraySort {
private static final InsertSort insertSort = new InsertSort();
@Override
public int[] sort(int[] sourceArray) throws Exception {
// 对 arr 进行拷贝,不改变参数内容
int[] arr = Arrays.copyOf(sourceArray, sourceArray.length);
return bucketSort(arr, 5);
}
private int[] bucketSort(int[] arr, int bucketSize) throws Exception {
if (arr.length == 0) {
return arr;
}
int minValue = arr[0];
int maxValue = arr[0];
for (int value : arr) {
if (value < minValue) {
minValue = value;
} else if (value > maxValue) {
maxValue = value;
}
}
int bucketCount = (int) Math.floor((maxValue - minValue) / bucketSize) + 1;
int[][] buckets = new int[bucketCount][0];
// 利用映射函数将数据分配到各个桶中
for (int i = 0; i < arr.length; i++) {
int index = (int) Math.floor((arr[i] - minValue) / bucketSize);
buckets[index] = arrAppend(buckets[index], arr[i]);
}
int arrIndex = 0;
for (int[] bucket : buckets) {
if (bucket.length <= 0) {
continue;
}
// 对每个桶进行排序,这里使用了插入排序
bucket = insertSort.sort(bucket);
for (int value : bucket) {
arr[arrIndex++] = value;
}
}
return arr;
}
/**
* 自动扩容,并保存数据
*
* @param arr
* @param value
*/
private int[] arrAppend(int[] arr, int value) {
arr = Arrays.copyOf(arr, arr.length + 1);
arr[arr.length - 1] = value;
return arr;
}
}
2.10、基数排序
基数排序是一种非比较型整数排序算法,其原理是将整数按位数切割成不同的数字,然后按每个位数分别比较。由于整数也可以表达字符串(比如名字或日期)和特定格式的浮点数,所以基数排序也不是只能使用于整数。
- 基数排序:根据键值的每位数字来分配桶;
- 计数排序:每个桶只存储单一键值;
- 桶排序:每个桶存储一定范围的数值;
/**
* 基数排序
* 考虑负数的情况还可以参考: https://code.i-harness.com/zh-CN/q/e98fa9
*/
public class RadixSort implements IArraySort {
@Override
public int[] sort(int[] sourceArray) throws Exception {
// 对 arr 进行拷贝,不改变参数内容
int[] arr = Arrays.copyOf(sourceArray, sourceArray.length);
int maxDigit = getMaxDigit(arr);
return radixSort(arr, maxDigit);
}
/**
* 获取最高位数
*/
private int getMaxDigit(int[] arr) {
int maxValue = getMaxValue(arr);
return getNumLenght(maxValue);
}
private int getMaxValue(int[] arr) {
int maxValue = arr[0];
for (int value : arr) {
if (maxValue < value) {
maxValue = value;
}
}
return maxValue;
}
protected int getNumLenght(long num) {
if (num == 0) {
return 1;
}
int lenght = 0;
for (long temp = num; temp != 0; temp /= 10) {
lenght++;
}
return lenght;
}
private int[] radixSort(int[] arr, int maxDigit) {
int mod = 10;
int dev = 1;
for (int i = 0; i < maxDigit; i++, dev *= 10, mod *= 10) {
// 考虑负数的情况,这里扩展一倍队列数,其中 [0-9]对应负数,[10-19]对应正数 (bucket + 10)
int[][] counter = new int[mod * 2][0];
for (int j = 0; j < arr.length; j++) {
int bucket = ((arr[j] % mod) / dev) + mod;
counter[bucket] = arrayAppend(counter[bucket], arr[j]);
}
int pos = 0;
for (int[] bucket : counter) {
for (int value : bucket) {
arr[pos++] = value;
}
}
}
return arr;
}
/**
* 自动扩容,并保存数据
*
* @param arr
* @param value
*/
private int[] arrayAppend(int[] arr, int value) {
arr = Arrays.copyOf(arr, arr.length + 1);
arr[arr.length - 1] = value;
return arr;
}
}
3、搜索算法
3.1、二分法
3.2、BFPRT(线性查找算法)
BFPRT算法:从n个元素中选出第k小或第k大的元素,同时也能选出前k小或前k大的所有元素。
BFPRT算法有点类似于快速排序算法,快速排序每一趟Partion的过程中一般都是选择第1个元素作为pivot,将小于pivot的元素交换到左边,将大于pivot的元素交换到右边,然后将pivot插入到它们的中间,最终得到的序列——pivot左边的元素都小于等于pivot,pivot右边的元素都大于等于pivot。BFPRT在Partion的过程中与快速排序不同的是它不是选择第1个元素作为pivot,而是对序列递归求取中位数,以该中位数作为pivot对序列进行划分。
时间复杂度:最坏情况的时间复杂度是O(n)
- 将序列中所有元素按5个元素一组进行划分,最后一组可能少于5个元素,对每一组元素进行插入排序选出中间的元素即为中位数;
- 对所有中位数重复步骤1,即对中位数进行分组,求得它们的中位数;重复此步骤,直到只有一个中位数;
- 遍历序列,得到该中位数的下标;
- 以该中位数作为pivot,对序列进行Partion划分过程,返回划分后的中位数的下标;
- 根据下标得出当前中位数是第X小元素,判断X是否等于K,若是则表明该中位数即为第K小元素,返回下标;否则,判断X是否大于K,若是,则从中位数左边的元素中找出第K小元素;否则,从中位数右边的元素中找出第X-K小元素(因为经过Partion后,中位数左边的元素都小于等于该中位数)。
https://blog.csdn.net/softimite_zifeng/article/details/77103544