>>>时间复杂度
计算时间复杂度
主要是看语句执行的次数
举例
例一 O(N)
核心就是找到当循环结束时,循环体执行了多少次
看循环的条件 i<n的时候终止 所以n与i有关,我们需要找出i与n的关系,我们先找出执行次数与i的值的关系 即 i = 2x+1
循环次数 1 2 3 4 。。。 x
i的值 3 5 7 9 。。。 2x+1
所以得到当2x+1 >= n
时 即循环到第x时,循环终止 能得到循环中的语句执行了x次 将x用n表示 x= (n-1)/2
即循环了 (n-1)/2次 所以执行了 (n-1)/2次 即T(n) = (n-1)/2 省去常数和n的系数,所以时间复杂度为O(n)
void fun1(int n)
{
int i=1,k=100;
while(i<n)
{
k=k+1;
i+=2;
}
}
例二
O(log2N)
int i = 1;
while(i<n){
i = i * 2;
}
可以得出,终止条件与i的值有关。我们列举出循环次数x与i的关系 得出 i = 2^x
循环次数x 1 2 3 4 ... x
i的值 2 4 8 16 ... 2^x
因为当i<n时循环结束,即 2^x < n ,此时语句执行了x次, x用n表示 x = log2N (以2为底)
, 即当执行log2N次时,循环结束,即一共执行了log2N次,所以时间复杂度就是 O(log2N)
;
例三
O(n^2)
平方阶O(n²) 就更容易理解了,如果把 O(n) 的代码再嵌套循环一遍,它的时间复杂度就是 O(n²)
,这段代码其实就是嵌套了2层n循环,它的时间复杂度就是 O(n*n),即 O(n²) 如果将其中一层循环的n改成m,那它的时间复杂度就变成了 O(m*n)
for(int i = 1;i<=n;i++){
for(int j = 1; j<=n;j++){
xxx...
}
}
常见时间复杂度
排序算法的时间复杂度
排序算法的稳定性解释
简单地说就是所有相等的数经过某种排序方法后,仍能保持它们在排序之前的相对次序,我们就说这种排序方法是稳定的。反之,就是非稳定的。
选择排序是不稳定的,举个例子:
序列5 8 5 2 9,第一遍选择第1个元素5会和2交换,那么原序列中2个5的相对前后顺序就被破坏了,所以选择排序不是一个稳定的排序算法
排序算法总结
1.冒泡排序(两两比较并交换)
1.1图示
(从小到大进行排序)
有点类似双指针,一个指针p1从头开始,一个指针p2从下一位开始,如果p1指向的元素大于p2指向的元素,那么交换两个数,然后p1走到p2的位置,P2继续后移,这样做会将最大的元素放到后面,
循环次数分析
外层循环
假设有n个数,根据上面的图示,我们会将n-1
个最大数放到数组的最后,所以外层需要循环n-1次,每一次找到一个大数放到后面
内层循环
然后每一趟的话,我们需要使用指针来对元素进行一一的比较,然后找出这一趟的最大值放到最后,理论上说我们每一次需要比较n-1
次,即循环n-1次,但是我们知道第一次循环会将整个数组的最大数放到最后,需要循环(n-1)次,那么第二趟就无需比较最后一个数和它前面的数了,那么需要比较(n-2次),依次类推,每一趟我们需要比较的元素个数(即内层循环次数)就是
n-1-(趟数(从0开始))
即第一趟需要比较 n-1-0
个元素
第二趟需要比较 n-1-1个
元素
。。。。
1.2代码分析(+优化)
public void bubbleSort(int[] nums) {
//外层循环 控制趟数
for (int i = 0; i < nums.length -1 ; i++) {
//设置优化标志位 当某一次的过程中 没有发生过数据的交换 说明所有元素都已经有序就不需要在进行了 直接break即可
boolean flag = true;
int temp = 0;
//内层循环 控制元素比较的次数
for (int j = 0; j < nums.length - 1 - i; j++) {
//j相当于前指针 j+1相当于后指针 前面的数比后面的大 就交换
if (nums[j] > nums[j + 1]) {
//将这一趟的flag置为false,说明交换过元素 即数组还不是有序的
flag = false;
temp = nums[j];
nums[j] = nums[j + 1];
nums[j + 1] = temp;
}
}
//flag为true 说明这一趟没有发生过交换 即数据都是有序的 直接结束
if (flag == true) {
break;
}
}
}
1.3算法分析
根据1.2的代码实现,可以得到,
- 冒泡排序的时间复杂度是
O(n^n)
循环嵌套
平均时间复杂度是O(n^n)
,最坏的时间复杂度也是O(n^n)
,
- 空间复杂度是
O(1)
,没有开辟另外的空间,直接在原数组中进行操作 稳定的排序 关键字相同的记录不会交换次序
2.快速排序(递归排序)*
2.1图示
2.2思路分析
快速排序的核心就是分区间处理
- 第一步:确定一个
分界点值mark
,可以是数组中的任意一个数 - 第二步:调整区间,使得整个区间一分为二时,左区间的数
[]
都是小于等于mark的,右区间的值(]
都是大于mark的,注意,分界点值不一定是mark - 第三步:递归处理左右两个区间
如何调整区间
- 最简单的方法就是新开两个数组a和b,遍历两次原数组,比mark大的放a数组,比mark小的放b数组,然后依次重新为原数组填充数据 但是这种需要使用额外空间,不推荐使用
- 我们可以使用双指针算法,一个指针l从头走,一个指针r从尾部走,l指针指向的数如果大于等于mark时停下,r指针如果小于等于mark时停下,交换两个数,如果r指针和l指针相遇或者r指针<l指针时结束,
此时r指针左边的数(包括r指针指向的数)都是小于等于mark的,r指针右边的数(不包括r指针指向的数)都是大于等于mark的
l指针左边的数(不包括l指针指向的数)都是小于等于mark的,l指针右边的数(包括l指针指向的数)都是大于等于mark的
举例:
比如有下面一组数 我们以array[0]即5为mark 划分两个区间 然后双指针
5 6 7 2 3 8 9
l指针第一次指向5等于mark 停下,然后r指针指向9大于mark继续向前走,指向8大于mark继续向前,然后走到3的位置小于mark停下,此时交换l指针和r指针指向的数,数组变为
3 6 7 2 5 8 9
l继续向后走,指向6大于mark停下,r指针向前走,指向2停下,交换 数组变为
3 2 7 6 5 8 9
此时l继续向后走 指向了7大于mark 停下,r继续往前走也指向7大于mark继续向前走,指向2停下,但是现在r指针的索引已经小于了l指针的索引 无需交换并且结束循环 最终数组变为
3 2 7 6 5 8 9 r最终指向2 l最终指向7
所以可以得出上面的结论,
`此时r指针左边的数(包括r指针指向的数)[3,2]都是小于等于mark(5)的,r指针右边的数(不包括r指针指向的数)[7,6,5,8,9]都是大于等于mark(5)的 ` `l指针左边的数(不包括l指针指向的数)[3,2]都是小于等于mark(5)的,l指针右边的数(包括l指针指向的数)[7,6,5,8,9]都是大于等于mark的`
所以说我们可以按照 [left,r] [r+1,rigth]
或者是 [left,l-1] [l,rigth]
划分两个区间,这样第一个区间的数都是小于等于mark的,第二个区间的数都是大于等于mark的
2.3代码
public void quickSort(int[] nums, int left, int right) {
//终止条件 当区间只有一个数时无需排序
if (left >= right) return;
//找mark值,并声明两个指针 l和r取边界的前(后)一位 这里为了方便下面在指针移动时停下交换数字后还得移动指针,我们使用的是do-while循环,即上来先做一次移动,代码看起来简洁一点
int mark = nums[left], l = left - 1, r = right + 1;
//当两个指针相遇或者r指针走到l前面时结束
while (l < r) {
//do-while比while的好处就是,这里如果两个指针同时停下时并且l<r交换完毕,我们还得再次的移动一次指针,否则会一直的死循环,do-while的话即使是俩指针交换完数依然会走,不用多加指针移动的代码
do l++; while (nums[l] < mark); //大于等于mark时停下
do r--; while (nums[r] > mark); //小于等于mark时停下
//l<r交换
if (l < r) {
int temp = nums[l];
nums[l] = nums[r];
nums[r] = temp;
}
}
//递归处理左区间
quickSort(nums, left, r);
//递归处理右区间
quickSort(nums, r + 1, right);
//这里的区间也可以写 (left,l-1) (l,rigth) 上面分析过
}
注意:
上面的代码,如果我们选nums[left]
为mark值时,我们在递归处理时不能使用以l
表示的左右区间,如果我们选nums[right]
为mark值时,在递归处理时不能使用以r
表示的左右区间 否则会出现死循环问题
例:
现在有一个数组 元素为[1,2]
如果我们选的mark是nums[left]即1
我们递归时用的是以l表示的两个区间 就会出现死循环的问题
此时l指针指向1等于mark,停下,r指针指向2大于mark向前走,此时l和r相遇,结束循环,此时l的索引为0 r的索引为0
- 此时以l指针表示的两个区间是 [0,-1] [0,1] 我们发现,我们一开始的区间就是[0,1]这里又回到了[0,1]程序就陷入了死循环
- 此时r指针表示的两个区间就是 [0,0] [1,1]就不会发生死循环
2.3算法分析
-
平均时间复杂度
是O(nlogn) (区间长度基本一致)
,最差的时间复杂度O(n^n)每次找的mark都是最大(最小)
, -
是一个
不稳定
的排序 -
需要额外的空间
O(nlogn)
因为使用了递归,使用了栈空间
2.4应用
3.简单选择排序(遍历交换)
3.1图示
从小到大排序
跟冒泡排序的思路差不多,也是每一次循环确定一个最小(最大值)
,不同的是,选择排序不需要像冒泡排序那样每一趟都两两的比较并交换元素,选择排序是每一趟使用一个变量在遍历的时候确定最小值的索引位置,然后遍历完成之后将这个元素与前面的元素进行交换,保证每一趟都确定一个元素
如下图所示,每一趟都确定一个最小的元素放到前面,下一次遍历的时候从已确定位置元素的下一个进行遍历
循环次数分析
外层循环
n个元素,需要确定n-1个元素的位置,所以外层循环需要n-1
次
内层循环
因为我们每一趟都将最小元素放到最前面,所以下一次遍历的时候先将未确定位置元素记录下来,然后从这个元素的下一个位置开始遍历,最后将这个未确定位置元素给确定下来
3.2代码分析
public void selectSort(int[] nums) {
//控制趟数 需要n-1趟
for (int i = 0; i < nums.length-1; i++) {
//minIndex指向的是未确定位置元素的首个
int minIndex = i;
//从i+1开始遍历 一直到最后 找到比minIndex索引位置元素还小的 更新minIndex
for (int j = i + 1; j < nums.length ; j++) {
if (nums[minIndex] > nums[j]) {
minIndex = j;
}
}
//这里判断最小元素的位置是否就是起始位置,如果是的话就无需交换,不是的话交换
if (minIndex != i) {
int temp = nums[i];
nums[i] = nums[minIndex];
nums[minIndex] = temp;
}
}
}
3.3算法分析
- 冒泡排序的时间复杂度是
O(n^n)
循环嵌套
平均时间复杂度是O(n^n)
,最坏的时间复杂度也是O(n^n)
,
- 空间复杂度是
O(1)
,没有开辟另外的空间,直接在原数组中进行操作 - 速度比冒泡排序快,因为冒泡排序交换元素的次数比较多,而选择排序交换元素的次数叫冒泡排序少
不稳定的排序
关键字相同的记录会交换次序
4.堆排序
5.插入排序(插入有序数组)
3.1图示
就是将第一个元素看成有序表,然后将后面的元素按照大小插入到有序表中
3.2代码
3.2.1初始版
将一个元素比如A插入到他前面的一个有序的数组中,无非就三种情况
- 要么A比第一个数都小,那么先将这个有序数组中的所有值后移,然后将A插到第一个位置
- A在数组的两个值之间
(比如...<B<=A<C<....)
,将C后面的元素后移,将A插入到C的位置 - A比这个有序数组的最大元素都大,那么A就不需要移动
public void insertSort(int[] nums) {
//外层循环控制插入元素的次数 一共n个元素 需要插入n-1次
for (int i = 1; i < nums.length; i++) {
//这个for循环是这个待插入元素前面的有序数组看成是一个区间 让待插入元素在这个区间中判断然后找位置
for (int j = 0; j <= i - 1; j++) {
//待插入元素比最后一个元素大 啥也不用干
if (nums[i] > nums[i - 1]) {
} else
//在待插入数组之间 先讲这个待插入元素存起来 然后将比待插入元素大的数全 部后移 最后将这个数插入
if (nums[i] >= nums[j] && nums[i] < nums[j + 1]) {
int t = nums[i];
for (int k = i; k > j + 1; k--) {
nums[k] = nums[k - 1];
}
nums[j + 1] = t;
//待插入元素比有序数组第一个元素都小 将有序数组全部后移 将待插入元素插入
} else if (nums[i] < nums[0]) {
int t = nums[i];
for (int k = i; k > 0; k--) {
nums[k] = nums[k - 1];
}
nums[0] = t;
}
}
}
}
3.2.2普通版
上面的代码虽然可以实现插入排序,但是嵌套了三层for循环,且后两个判断基本做的都是相同的事,都是先将数组后移,然后插入数据,所以我们可以使用一个while循环,将这三种情况三合一 (这种做法跟图示相同,都是判断着并移动着元素)
public void insertSort(int[] nums) {
//元素从索引为1开始到最后都得插入
for (int i = 1; i < nums.length; i++) {
//待插入元素 用一个变量存起来
int insertValue = nums[i];
//使用一个指针 指向有序数组的末尾 最后记录的是待插入元素的前一个位置
int index = i - 1;
/*
1、当待插入元素大于末尾元素时,进不来
2、当待插入元素小于末尾元素且index>0时 将元素后移 并且index后移 直到某一个元素小于待插入元素时结束循环 这时待插入元素刚好在一个正确的区间内 这时index指向的是待插入位置的前一位
3、当循环到index=-1时结束,说明待插入元素比第一个元素还小,
*/
//当index=-1 或者找到一个元素比待插入元素小结束循环
while (index > 0 && insertValue < nums[index]) {
//将待插入元素大的数后移
nums[index + 1] = nums[index];
//指针后移 继续指向
index--;
}
//index+1就是待插入位置
nums[index + 1] = insertValue;
}
}
3.3算法分析
- 选择排序的时间复杂度是
O(n^n)
循环嵌套
平均时间复杂度是O(n^n)
,最坏的时间复杂度也是O(n^n)
,
- 空间复杂度是
O(1)
,没有开辟另外的空间,直接在原数组中进行操作 稳定的排序
(关键字相同的记录不会交换次序)
6.希尔排序(优化插入排序)
6.1优化插入排序
插入排序的核心就是将一个元素A插入到一段有序的数组中,我们A与前面的数组中的最后一个元素(最大元素)开始比较,然后依次将元素后移,最终找到A的待插入位置,
我们可以发现,当后面的元素都的位置大部分都在有序数组中间时,我们会花费大量的时间在移动数组和比较中,所以我们在插入排序的基础上优化一下,(图示见6.2)
优化的思路就是我们每次按照下标对元素进行分组,对于每组进行插入排序,这样在最后进行整体插入排序时,大部分的小元素都在数组的前半部分,大的元素都在后半部分,这样的好处就是元素移动的次数明显减少。
6.2图示
现在有7个数字 0 1 2 3 4 5 6
6 7 4 3 8 9 2
一、
我们将7/2 = 3 即第一次索引每+3分一组
下标为 0 3 6 的一组 即 6 3 2
下标为 1 4 的一组 即 7 8
下标为 2 5 的一组 即 4 9
对每一组的数进行插入排序后变为
0 1 2 3 4 5 6
2 7 4 3 8 9 6
二、我们将3/2 =1 这一次索引每+1分一组 即所有元素一组 我们整体进行插入排序
0 1 2 3 4 5 6
2 3 4 5 6 8 9
6.3代码
public void shellSort(int[] nums) {
int len = nums.length;
while (len >= 1) {
//记录一下每一次的元素的下一个元素的索引增量
int p = len / 2;
for (int i = 0; i < nums.length; i++) {
//找到有序数组的最后一个位置
int index = i - p;
//存一下待插入元素
int insertValue = nums[i];
//循环判断 找到待插入元素的位置
while (index >= 0 && insertValue < nums[index]) {
//待插入元素小于有序数组的最后一个元素时 移动有序数组的元素
nums[index + p] = nums[index];
//index指针继续向前走 找到有序数组的前一个元素在进行比较
index -= p;
}
//最后index会指向待插入索引位置的前一个 所以+p才是待插入元素的位置
nums[index + p] = insertValue;
}
//每次len都必须/2
len /= 2;
}
}
6.4算法分析
-
选择排序的平均时间复杂度是
O(nlogn)
最坏的时间复杂度也是O(n^s) 1<s<2
, -
空间复杂度是
O(1)
,没有开辟另外的空间,直接在原数组中进行操作 -
不稳定的排序
(关键字相同的记录会交换次序)
7.归并排序(递归合并)*
7.1思路
归并排序的核心思想是将两个有序的数列合并成一个大的有序的序列。通过递归,层层合并,即为归并 分治
归并的总体示意图
是外部排序,需要借助额外的空间
- 1.从中间划分两个区间
- 2.分别递归两个区间
- 3.合并
递归区间示意图
每次划分n*2个区间,当区间中只有一个数时,终止递归,然后向上合并,这里最开始合并的就是80和30,因为80和30区间只有一个数,然后合并60和20,然后时30 80 与20 60进行合并,然后右半部分也是同理,最后异一步合并第二层的两个数组,最终变成一个有序数组
核心的部分就是合并,其实就是给你两个有序的数组,将这两个数组合并,要求合并之后还是有序的,
这里使用双指针算法,分别从两个数组的头部开始走,判断哪个数小,小的数放到新数组中并且指针后移继续比较,最后那个指针走到头了,就将另外一个数组的后部分都接上
7.2代码
public void merge_sort(int[] arr, int l, int r) {
// 递归结束条件 区间只有一个数无序划分区间直接返回
if (l >= r) return;
// 逻辑部分 找中心点,然后分别向左右两边递归
int mid = l + ((r - l) >> 1);
//左区间是[l,mid]
merge_sort(arr, l, mid);
//右区间是[mid+1,r];
merge_sort(arr, mid + 1, r);
//双指针算法合并两个有序数组
int[] tmp = new int[r - l + 1];
int i = l, j = mid + 1, k = 0;
while (i <= mid && j <= r) {
if (arr[i] <= arr[j])
tmp[k++] = arr[i++];
else
tmp[k++] = arr[j++];
}
//那个指针没走完将后面的数全部接上
while (i <= mid) tmp[k++] = arr[i++];
while (j <= r) tmp[k++] = arr[j++];
// 给原数组赋值
for (i = l, j = 0; i <= r; i++, j++)
arr[i] = tmp[j];
}
7.3算法分析
时间复杂度是O(nlogn)
假设有n个数,一共要分logn层,每层都是O(n),所以是nlogn
需要额外的空间