写在前面
复杂度
时间复杂度
- 本质上就是函数的增长趋势,避免指数阶的算法
- 时间频度,
T(n)
,出一次结果,所操作的循环次数,一个for循环为n,两个并列的for循环为2n,两个嵌套的for循环为n^2,忽略常数项、低次项(数量级足够大)、系数(次方可以,次方以上不行)
- 时间复杂度,
O(n)
省略T(n)
的常数项、低次项、系数
类型
- 常数阶,无论执行了多少行,只要没有循环,就是O(1),不会随着变量的增长而增长
- 对数阶,
log2n
,2^x = n
,也就是说执行了x
次才推出,对数阶也就是x
int i = 1
while (i < n) {
i = i * 2
}
- 线性阶,一次n次的循环
- 线性对数阶,线性阶循环嵌套对数阶
- 平方阶,线性阶嵌套线性阶
- 指数阶,递归,进行n行n列的嵌套递归,例如如果8皇后问题,不限制
max
大小,将无限往下走;如果,一个2次循环嵌套n次就是O(2^n)
空间复杂度
- 耗费的内存空间,一个算法临时占用的存储空间大小
- 实际开发中,不考虑硬件成本,大部分算法采用
空间换时间
的方式来优化
排序算法
- 内部排序,将需要处理的所有数据都加载到内部存储器中进行排序
- 外部排序,数据量过大,无法全部加载到内存,需要接触外部存储进行
冒泡排序
- 通过对待排序的序列从前向后,从下标较小的元素开始,依次比较相邻元素的值,若发现前面的大则交换,最终形成
升序
,从前向后,就像是气泡一样往上冒
- 优化,排序中,各元素不断接近自己的位置,如果一趟比较下来没有进行过交换,就说明序列有序,因为通过设置
flag标志位
避免不必要的排序
- 一共进行
数组大小-1次
排序,两两比较,第一趟比出最大,第二趟比出第二大,以次类推
- 因为
大
是具有传递性的,从头开始比,比到最后的数一定是比前面的所有数都大的
//3,9,-1,10,-2
int[] arr = {3, 9, -1, 10, -2}
int temp = 0
//第一趟排最大,第二趟排倒数第二大....
//0 1 2 3
//-1 -2 -3 -4
for (int j = 0
for (int i = 0
if (arr[i] > arr[i+1]) {
temp = arr[i]
arr[i] = arr[i+1]
arr[i+1] = temp
}
}
System.out.println(Arrays.toString(arr))
}
优化
int[] arr = {-1, 2, 3, 10, 9}
int temp = 0
//标识,是否进行过变化
boolean flag = false
for (int j = 0
for (int i = 0
if (arr[i] > arr[i+1]) {
flag = true
temp = arr[i]
arr[i] = arr[i+1]
arr[i+1] = temp
}
}
System.out.println("第"+(j+1)+"趟"+Arrays.toString(arr))
if (flag == false) {
//一趟排序一次都没有发生过
break
} else { //如果发生过变换,重新置回false,记录下一次
flag = false
}
}
选择排序
- 从欲排序的数据中,按指定的规则选出某一个元素,再按规定交换位置
- 第一次,从
0~n-1
选出最小值与arr[0]
交换,第二次从1~n-1
选取最小值,与arr[1]
交换,以此类推
//假设第一个最小
int index = 0
int min = 0
for (int i = 0
//假设第i个最小,每次都从第i开始假设,并每一轮把最小的排在前面
index = i
min = arr[i]
//i=0时,与后面的所有进行比较,已经是最小的了,放在最前面
//从第i开始,每轮比较出第i个最小的,放在前面
for (int j = 0 + i
//如果最小值不是这个,保留新的最小值
if (min > arr[j]) {
min = arr[j]
index = j
}
}
//且如果,不是假设的最小值,才要改变
if (index != i) {
//确定最小值后,把最小值放到最前面
//将最小值放在索引为0的位置
arr[index] = arr[i]
arr[i] = min
}
}
插入排序
- 类似玩扑克牌,一堆乱序的牌,先拿起一张,当拿起第二张的时候与第一张相比;当拿起第三张时,与前两张相比,并且先于前两张的最大一张先比···
- 把n个排序的元素看为一个有序表和一个无序表,开始时有序表只包含一个元素,无序表中包含n-1个元素,排序过程中每次从无序表中取出第一个元素,把它依次与有序表进行比较,并将其插入
- 只需要插入
length-1
次
- 时间复杂度,
O(n^1)
~O(n^2)
- 把
insertIndex
理解为指针,寻找位置
//第一轮 {{101}, {34, 119, 1}} {101}为有序表 {34, 119, 1}为无序表,34要在{101}中找位置
int insertVal = 0
int insertIndex = 0
for (int i = 1
insertVal = arr[i]
insertIndex = i - 1
//带插入的数还没找到位置,就需要将insertIndex后移
while (insertIndex >= 0 && insertVal < arr[insertIndex]) {
arr[insertIndex + 1] = arr [insertIndex]
insertIndex --
}
//如果待插入的值,与目前数组中的相同位置的数一样,没有改变,则不需要改变
//insertIndex+1 = i
if (insertVal != arr[i]) {
//由于循环中每次都-1,所以跳出循环找到添加的位置,实际上要少1,故要加上
//当退出循环,说明插入的位置找到 insertIndex + 1
arr[insertIndex + 1] = insertVal
}
}
希尔排序
- 分组排序
- 缩小增量排序,是一种插入排序。按下标的一定增量进行分组,对每组使用直接插入排序进行排序,随着增量逐渐减少,当增量为1时,在进行插入排序,得到最终的结果
- 举例,十个数
10/2
,按照相同的步长分成5组
(增量),各自进行插入排序;再5/2
,按照相同的步长分成2组
,在进行插入排序;最后2/2
1组,进行插入排序
交换法
int temp = 0
//8, 9, 1, 7, 2, 3, 5, 4, 6, 0
// i=5 6 7 8 9 j=0 1 2 3 4
// {5, 0}比较 {6 1}比较....
for (int i = 5
//遍历各组中所有元素(共五组,每组有两个元素)
for (int j = i - 5
if (arr[j] > arr[j+5]) {
//如果 {5 0}比较 arr[0] > arr[5]
temp = arr[j]
arr[j] = arr[j+5]
arr[j+5] = temp
}
}
}
System.out.println(Arrays.toString(arr))
//第二轮
//i = 2, 3, 4, 5, 6, 7, 8, 9
//j = 0 1 2 0 3 1 4 2 0 5 3 1 6 4 2 0 7 5 3 1
for (int i = 2
//遍历各组中所有元素(共2组,每组有5个元素)
for (int j = i - 2
if (arr[j] > arr[j+2]) {
temp = arr[j]
arr[j] = arr[j+2]
arr[j+2] = temp
}
}
}
System.out.println(Arrays.toString(arr))
for (int i = 1
//遍历各组中所有元素(共2组,每组有5个元素)
for (int j = i - 1
if (arr[j] > arr[j+1]) {
temp = arr[j]
arr[j] = arr[j+1]
arr[j+1] = temp
}
}
}
System.out.println(Arrays.toString(arr))
int temp = 0
for (int gap = arr.length / 2
for (int i = gap
for (int j = i - gap
if (arr[j] > arr[j+gap]) {
temp = arr[j]
arr[j] = arr[j+gap]
arr[j+gap] = temp
}
}
}
}
移动法
for (int gap = arr.length / 2
for (int i = gap
//插入
int insertVal = arr[i]
int insertIndex = i - gap
while (insertIndex >= 0 && insertVal < arr[insertIndex]) {
arr[insertIndex + gap] = arr[insertIndex]
insertIndex -= gap
}
if (insertVal != arr[i]) {
arr[insertIndex + gap] = insertVal
}
}
}
快速排序
- 对冒泡排序的改进,将要排序的数组分成两个部分,一部分都要比另一部分要小,通过递归实现
- 核心是,在每一次循环中,在进行分组递归,最终也是进行两个数的比较, 最后确定顺序
- 时间复杂度
O(nlog2n)
~O(n^2)
描述过程
- 快排的规则是,通过
不断让左边的数小于右边的数
,通过递归达到处理最后两个
,直到完成,依次退出递归。假设基数指向中间(也可以指向任意的位置)
,从左右两边
同时开始向中间靠拢,停下来的条件是在左边找到比基数大的数或者找到基数,左边停下
和在右边找到比基数小的数或者找到基数,右边停下
- 那么,根据以上停下的条件,就有了四种可能性
- 第一种,
左边找到大的,右边找到小的
- 第二种,
右边找到小的,左边找到基数的
- 第三种,
左边找到大的,右边找到基数
- 第四种,
左右同时找到基数
- 遇到第一种时,交换两个数的位置,接着找,直到达到其他三种可能
- 遇到第二种时,交换两个数位置,此时
右边将指向基数,左边将指向小的数
,如果此时左右之间仍有数
,那么将继续循环;如果此时左右之间没有数
,进行下一个规则左边+1指向基数
,此时左和右同时指向基数,并跳出循环
- 遇到第三种时,交换两个数位置,
左边将指向基数,右边将指向大的数
,如果此时左右之间仍有数
,那么将继续循环;如果此时左右之间没有数
,进行下一个规则右边+1指向基数
,此时还有下一个判断右边指向基数后,左边+1,指向下一个
,此时左边的位置大于右边的位置,跳出循环
- 遇到第四种时,直接跳出
- 那么,此时已经通过了
处理一层递归
,并要进行子递归的预处理,此时面对的上一层留下来的也有两种可能
- 第一种,
左右相等,指向同一个数
- 第二种,
左边大于右边的位置
- 遇到第一种时,需要进行
左边+1,右边-1
,使左边的位置大于右边的位置,原因是如果不进行这个处理,加入左右只有一个数(一共只有3个数或者两个数),那么将进行无限递归,因为无法达到左边>=右边临界值
和右边<=左边临界值
- 遇到第二种时,则不需要上述处理,因为此时左右已经错开,如果只有两个数或者三个数,左右也无法进入下一层递归;并且如果进行上述操作,反而会漏到中间的某个数,因为每次递归都要进行重新的基数选择
public static void quickSort(int[] arr, int left, int right) {
int l = left
int r = right
int pivot = arr[(left + right) / 2]
//负责交换左右两边的值
int temp = 0
//让pivot 左边放小的,右边放大的
while (l < r) {
//在左边找到>=pivot的值
//跳出条件,找到自身或者找到比自身大的
while (arr[l] < pivot) {
l += 1
}
//在右边找到<=pivot的值
while (arr[r] > pivot) {
r -= 1
}
//说明指向同一个,直接跳出
//因为能进上面两个循环的唯一条件就是 l<piovt的索引 和 r>pivot
//且当找到`arr[l] == pivot` `arr[r] == pivot` 上面两个循环会直接退出
//此时l==r
if (l == r) {
break
}
//交换左右两边的值
temp = arr[l]
arr[l] = arr[r]
arr[r] = temp
//如果交换完成出现`arr[l] == pivot` 说明`r`已经指向了`pivot`的索引
//此时,`piovt`右边都大于`pivot`,而交换完成后,需要让`r`向前走一步
//指向`pivot`原来位置的左边,也就是调整`pivot`的位置,再接着进行
if (arr[l] == pivot) {
r -= 1
}
//如果交换完成,arr[r] == pivot 说明中位数被换过来了,l++后移动,同上
//如果经过上一步`r`被调整到`pivot`,那么`l`将被+1,移到下一个,此时`l>r`
if (arr[r] == pivot) {
l += 1
}
}
//如果`l==r`,说明指向了`pivot`,那么说明在这个子树中,这个数的位置已经确定,将`l`后移和`r`前移即可;
//如果任意情况都移动,会漏掉中间的数;
//且当,先只有`arr[r] == pivot && arr[l] > pivot`的情况下,
//也就是`l指向比pivot大的数,而r指向pivot`,达到`arr[r] == pivot && arr[l] != pivot`的要求,与`pivot`交换位置,
//或者当`左边已经全部是小,右边已经全部是大`,达到`l == r`直接跳出,才会出现。
//如果此时,左右只有一个值,不需要进行子递归,需要通过`l+1 / r+1`跳出这次递归
if(l == r) {
l += 1
r -= 1
}
//左递归
if (left < r) {
quickSort(arr, left, r)
}
//右递归
if (right > l) {
quickSort(arr, l, right)
}
}
归并排序
- merge-sort
- 利用归并的思想实现,采用经典的分治策略,将问题
分
成一些小的问题,然后递归求解,而治
的阶段,将分
的阶段得到的各答案放在一起
- 分+合的方法
public static void mergeSort(int[] arr, int left, int right, int[] temp) {
if (left < right) {
int mid = (left + right) / 2;
mergeSort(arr, left, mid, temp);
mergeSort(arr, mid + 1, right,temp);
merge(arr, left, mid, right, temp);
}
}
/**
* @param arr 原始数组
* @param left 最左边
* @param mid 中间索引
* @param right 最右边
* @param temp 临时数组
*/
public static void merge(int[] arr, int left, int mid, int right, int[] temp) {
int i = left
int j = mid + 1
int t = 0
//先把左右两边有序的数据按照规则填充到temp数组
//直到左右两边的有序序列,又一遍处理完毕之后
while(i <= mid && j <= right) {
// 如果左边的有序序列的当前元素,<=右边的当前有序
// 将左边的移到temp数组中
if (arr[i] <= arr[j]) {
temp[t] = arr[i]
t ++
i ++
} else { //右边比左边小
temp[t] = arr[j]
t ++
j ++
}
}
//把有剩余数组的一边数组一次全部填充到temp
while (i <= mid) {
//左边的有序序列还有剩余元素,包括中间的元素
temp[t] = arr[i]
t ++
i ++
}
while (j <= right) {
//右边的有序序列还有剩余元素
temp[t] = arr[j]
t ++
j ++
}
//将temp数组的元素拷贝到arr
t = 0
int tempLeft = left
while (tempLeft <= right) {
//因为是从小数组逐渐合并到大数组,在合并小数组时,每次最左边的起始索引和右边的末端索引都不同,跟传来的一样
//但是temp数组,都是从0开始的
arr[tempLeft] = temp[t]
t ++
tempLeft ++
}
}
基数排序
- 空间换时间
- 属于
分配式
排序,桶子法
,通过键值的各个位的值,将要排序的元素分配至某些桶
中,达到排序的目的
- 将所有待比较数值统一为同样的数位长度,数位较短的数前面补零,然后从最低位开始,依次进行一次排序
- 先比较
个位数
,并且使相同数量级(只有个位数)
产生了顺序,在接下来比较十位数
的过程中,将相同数量级的数
放在一个桶中,又将个位数相同,十位数不同的数
比较了出来
- 举例,三个数
53 52 2
,第一次轮个位数的比较中
,52 2
放在一个桶中,结果为52 2 53
;此时对于各个数字的个位数来说已经有了顺序,例如52在53前面
。第二轮比较十位数
,52 53
在一个桶中,而2
在第一个桶中,在按照顺序放置的时候,52 53
按顺序放入一个桶中,而同时2与52
也比较出了一个结果
- 这就说明,基数排序(桶排序),是在一次排序中,同时比较了很多组,例如在比较十位数的过程中,同时比较了
个位数相同的组
和相同数量级的组
,并且如果十位数相等
,则得益于上一轮个位数的比较产生的顺序
轮次讲解
//定义一个二维数组,表示10个桶
//二维数组包含10个一维,为了防止溢出,每一个桶的大小为arr.length
int[][] bucket = new int[10][arr.length]
//为了记录每个桶中,实际存放了多少个数据,定义一个一维数组记录各个桶每次放入的数据个数
//bucketElementCounts[digitOfElement]表示第digitOfElement的数量
int[] bucketElementCounts = new int[bucket.length]
for (int j = 0
//取出个位
int digitOfElement = arr[j] % 10
//放入到对应的桶中 放入第几`digitOfElement`个桶的第几个位置`bucketElementCounts[digitOfElement]`
bucket[digitOfElement][bucketElementCounts[digitOfElement]] = arr[j]
//每个桶的个数
bucketElementCounts[digitOfElement]++
}
//按照这个桶的顺序,取出顺序,放入原来的数组
int index = 0
for (int k = 0
//循环第k个桶
//如果桶中有数据,放入原数组
if (bucketElementCounts[k] != 0) {
for (int l = 0
//循环k桶中元素的个数
//取出元素放入到arr
arr[index] = bucket[k][l]
//放入原数组的索引++
index ++
}
}
//每一轮轮处理后,需要把每个桶的数量清零 bucketElementCounts[k]
bucketElementCounts[k] = 0
}
for (int j = 0
//取出个位
int digitOfElement = arr[j] / 10 % 10
//放入到对应的桶中 放入第几`digitOfElement`个桶的第几个位置`bucketElementCounts[digitOfElement]`
bucket[digitOfElement][bucketElementCounts[digitOfElement]] = arr[j]
//每个桶的个数
bucketElementCounts[digitOfElement]++
}
//按照这个桶的顺序,取出顺序,放入原来的数组
index = 0
for (int k = 0
//循环第k个桶
//如果桶中有数据,放入原数组
if (bucketElementCounts[k] != 0) {
for (int l = 0
//循环k桶中元素的个数
//取出元素放入到arr
arr[index] = bucket[k][l]
//放入原数组的索引++
index ++
}
}
}
完全代码
bucketElementCounts数组
决定了第k个桶存的数量,当需要重新将数组复制到原数组时,bucket[k]桶的循环次数
由bucketElementCounts[k]
决定,每一轮过后都清零
//得到数组中最大的数的位数
int max = arr[0]
for (int i = 0
if (arr[i] > max) {
max = arr[i]
}
}
//最大的数的位数
int maxLength = (max + "").length()
//定义一个二维数组,表示10个桶
//二维数组包含10个一维,为了防止溢出,每一个桶的大小为arr.length
int[][] bucket = new int[10][arr.length]
//为了记录每个桶中,实际存放了多少个数据,定义一个一维数组记录各个桶每次放入的数据个数
//bucketElementCounts[digitOfElement]表示第digitOfElement的数量
int[] bucketElementCounts = new int[bucket.length]
for (int i = 0, n = 1
//从个位开始,依次进行排序
for (int j = 0
//取出个位
int digitOfElement = arr[j] / n % 10
//放入到对应的桶中
//放入第几`digitOfElement`个桶的第几个位置`bucketElementCounts[digitOfElement]`
bucket[digitOfElement][bucketElementCounts[digitOfElement]] = arr[j]
//每个桶的个数
bucketElementCounts[digitOfElement]++
}
//按照这个桶的顺序,取出顺序,放入原来的数组
int index = 0
for (int k = 0
//循环第k个桶
//如果桶中有数据,放入原数组,这里用记录的桶的数据个数作为判断
if (bucketElementCounts[k] != 0) {
for (int l = 0
//循环k桶中元素的个数,个数存在bucketElementCounts[k]中
//取出元素放入到arr
arr[index] = bucket[k][l]
//放入原数组的索引++
index ++
}
}
//每一轮轮处理后,需要把每个桶的数量清零 bucketElementCounts[k]
bucketElementCounts[k] = 0
}
}
处理负数
- 第一个思路,分开正负两个数组,处理复数的绝对值,然后还原
//分开两个数组
ArrayList<Integer> postive = new ArrayList<>()
ArrayList<Integer> negative = new ArrayList<>()
for (int i : originArr) {
if (i < 0) {
negative.add(Math.abs(i))
} else {
postive.add(i)
}
}
Integer[] postiveArr = postive.toArray(length -> new Integer[length])
Integer[] negativeArr = negative.toArray(length -> new Integer[length])
//排序
radixSort(postiveArr)
radixSort(negativeArr)
//负数数组倒序
int temp = 0
//对半 如果长度为8 i为 0 1 2 3 与 7 6 5 4 交换
//如果长度为7 i为 0 1 2 与 6 5 4 交换
for (int i = 0
//negativeArr.length - i - 1 分别可以取到 7 6 5 4
temp = negativeArr[negativeArr.length - i - 1] * -1
negativeArr[negativeArr.length - i - 1] = negativeArr[i] * -1
negativeArr[i] = temp
}
//还原
for (int i = 0
originArr[i] = negativeArr[i]
}
for (int i = 0
originArr[negativeArr.length + i] = postiveArr[i]
}
//加上特别大的数
for (int i = 0
arr[i] = arr[i] + 1000
}
//排序
radixSort(arr)
//还原
for (int i = 0
arr[i] = arr[i] - 1000
}