文章目录
前言
时间复杂度
概念
时间复杂度简单的说就是一个程序运行所消耗的时间,叫做时间复杂度,我们无法目测一个程序具体的时间复杂度,但是我们可以估计大概的时间复杂度。一段好的代码的就根据算法的时间复杂度,即使在大量数据下也能保持高效的运行速率,这也是我们学习算法的必要性。
时间复杂度表示形式
一般用O()来表示算法的时间复杂度,我们叫做大O记法。
时间复杂度规则
-
用常数1取代运行时间中的所有的加法常数。比如,一个程序中有十条输出语句,我们不会记成O(10),而是用O(1)来表示。
-
如果最高阶项不是1,那么去掉最高阶阶项,因为我们认为数字在后期影响不大。如O(2n),则时间复杂度应该为O(n)。
-
只保留最高阶项,如O(3n^2 +6n+2),则时间复杂度为O(n^2)。
常见的时间复杂度排序
O(1)< O(log2(n))< O(n)< O(nlog2(n)< O(n^2)< O(n^3)< O(2^n)< O(n!)< O(n^n)
空间复杂度
简单的说就是程序运行所需要的空间。写代码我们可以用时间换空间,也可以用空间换时间。加大空间消耗来换取运行时间的缩短加大时间的消耗换取空间,我们一般选择空间换时间。一般说复杂度是指时间复杂度。
更多请参考:【数据结构和算法】时间复杂度和空间复杂度
递归排序时间复杂度估算公式
master公式的使用
T(N) = a*T(N/b) + O(N^d)
N:
父问题的样本量;
a:
子问题发生的次数(父问题被拆分成了几个子问题,不需要考虑递归调用,只考虑单层的父子关系);
b:
被拆成子问题,子问题的样本量(子问题所需要处理的样本量),比如 N 被拆分成两半,所以子问题样本量为 N/2;
O(N^d):
剩余操作的时间复杂度,除去调用子过程之外,剩下问题所需要的代价[常规操作则为 O(1)];
a表示
- log(b,a) > d -> 复杂度为O(N^log(b,a))
- log(b,a) = d -> 复杂度为O(N^d * logN)
- log(b,a) < d -> 复杂度为O(N^d)
对数器
概念
存在一个想要测算法a时,可以使其结果集和另一个实现复杂度不好但是容易实现的方法b的结果集进行比较,由此检验算法正确与否。可以控制测试次数,测试样本,不依赖于线上测试平台。
测试步骤
-
实现一个随机样本产生器;
-
把方法a和方法b跑相同的随机样本,看看得到的结果是否一样;
-
如果有随机样本使得比对结果不一致,打印样本进行人工干预,对方法a或方法b进行修改;
-
当样本数量很多时比对测试依然正确,则可以确定方法a已经正确;
排序算法
冒泡排序
思路
将第1个元素和第2个元素进行比较,若为逆序则将两个元素交换,然后比较第2个元素和第3个元素。依次类推,直至第 n-1个元素和第 n个元素进行比较为止。上述过程称为第一趟冒泡排序,其结果使最大值元素被放置在最后一个位置(第 n个位置)。
然后进行第二趟冒泡排序,对前 n-1个元素进行同样操作,其结果是使第二大元素被放置在倒数第二个位置上(第 n-1个位置);
依次如此操作,直到最小元素放置到第1个位置,完成排序操作;
代码实现
private static void bubbleSort(int[] nums) {
if (nums == null || nums.length == 1) {
return;
}
/**
* 第一层指遍历范围 第一次冒泡遍历n-1次,后续n-2次...直到1次
*/
for (int end = nums.length - 1; end > 0; end--) {
/**
* 第二层进行数据交换
*/
for (int i = 0; i < end; i++) {
if (nums[i] > nums[i + 1]) {
int temp = nums[i];
nums[i] = nums[i + 1];
nums[i + 1] = temp;
}
}
}
}
复杂度分析
时间复杂度O(N^2),额外空间复杂度O(1)
选择排序
思路
先在[0~n-1]下标范围内找到最小值放到0位置上;
再在[1~n-1]下标范围内找到最小值放到1位置上;
依次如此操作,直到最后一个最小值【最大值】放在n-1位置上,完成排序操作;
代码实现
private static void selectSort(int[] nums) {
if (nums == null || nums.length <2) {
return;
}
for (int i = 0; i < nums.length - 1; i++) {
int minIndex = i;
for (int j = i + 1; j < nums.length; j++) {
minIndex = nums[j] < nums[minIndex] ? j : minIndex;
}
/**
* 交换i和minIndex位置的数
*/
int temp = nums[i];
nums[i] = nums[minIndex];
nums[minIndex] = temp;
}
}
复杂度分析
时间复杂度O(N^2),额外空间复杂度O(1)
插入排序
思路
下标第0位置元素默认已排好序,当前排好序范围[0~0];
下标第1位置元素和第0位置元素比较大小,若小于第0位置元素则交换位置,排好序范围[0~1];
下标第2位置元素和第1位置元素比较大小,若不小于,则不交换位置,若小于第1位置元素,则交互位置,继续和0位置比较,若仍小于第0位置元素,继续交换位置,最终排好序范围[0~2];
依次如此操作,直到完成最终排序;
代码实现
private static void insertSort(int[] nums) {
if (nums == null || nums.length <2) {
return;
}
for (int i = 1; i < nums.length; i++) {
//如果j位置元素>j+1位置元素,不断交换位置,直到比较到0位置
for (int j = i - 1; j >= 0 && nums[j] > nums[j + 1]; j--) {
int temp = nums[j];
nums[j] = nums[j+1];
nums[j+1] = temp;
}
}
}
复杂度分析
时间复杂度O(N^2),额外空间复杂度O(1)
归并排序
思路
- 每次将数据划分成两部分,并分别对两部分进行排序;
- 每次合并排序好的两部分,依次移动两部分元素下标,找到最小值,依次放置到合并数组中,经过多次合并数组,最终完成排序操作;
代码实现
private static void mergeSort(int[] nums) {
if (nums == null || nums.length <2) {
return;
}
mergeSort(nums, 0, nums.length - 1);
}
private static void mergeSort(int[] nums, int l, int r) {
if (l == r) {
return;
}
int mid = l + (r - l) / 2;
//对前半部分进行排序
mergeSort(nums, l, mid);
//对后半部分进行排序
mergeSort(nums, mid + 1, r);
//合并两者排序
merge(nums, l, mid, r);
}
private static void merge(int[] nums, int l, int mid, int r) {
int[] help = new int[r - l + 1];
int i = 0;
int p1 = l;
int p2 = mid + 1;
while (p1 <= mid && p2 <= r) {
//谁小放谁,并移动对应数组位置
help[i++] = nums[p1] < nums[p2] ? nums[p1++] : nums[p2++];
}
while (p1 <= mid) {
//说明p2已遍历完毕,后续位置填充p1没遍历完的元素
help[i++] = nums[p1++];
}
while (p2 <= r) {
//说明p1已遍历完毕,后续位置填充p2没遍历完的元素
help[i++] = nums[p2++];
}
for (i = 0; i < help.length; i++) {
nums[l + i] = help[i];
}
}
复杂度分析
时间复杂度代入递归master公式 T(N) = 2T(N/2)+O(N) ,即
时间复杂度O(N*logN),额外空间复杂度O(N)
快速排序
思路
- 从给定数组中选择一个数作为比较值target,将数组中的元素不断与target进行比较交换,从而将数组整体划分成三个部分【
<target
,=target
;>target
】,并记录=target部分的起始下标和结束下标
; - 根据步骤1的下标分别对
<target
、>target
部分继续上述划分操作,最终完成整个数组排序;
代码实现
/**
* 快速排序
*
* @param aar 目标数组
* @param l 需要排序的起始位置,传0表示从头开始
* @param r 需要排序的结束为止,传arr.length-1表示结束位置
*/
private static void quickSort(int[] aar, int l, int r) {
if (aar == null || aar.length < 2) {
return;
}
if (l < r) {
//1.先随机选择数组中的一个数,交换到数组最后一个位置,作为比较交换的target,这样做为了概率处理,避免特定情况(比如数组是逆序)造成的多余划分问题
swap(aar, r, (int) (l + (r - l + 1) * Math.random()));
//2.在l~r下标范围内,以arr[r]作为target,划分出三部分区域[<target, =target ,>target],并返回 =target范围的第一个下标和最后一个下标数组
int[] p = quickSortPartition(aar, l, r);
//3.递归对<target范围排序
quickSort(aar, l, p[0] - 1);
//3.递归对>target范围排序
quickSort(aar, p[1] + 1, r);
}
}
private static int[] quickSortPartition(int[] aar, int l, int r) {
//取数组最后一个作为比较目标值
int target = aar[r];
//定义小于target的数组下标
int less = l - 1;
//定义大于target的数组下标
int more = r + 1;
while (l < more) {
if (aar[l] < target) {
//小于目标值,划分到小于区域
swap(aar, ++less, l++);
} else if (aar[l] > target) {
//大于目标值,划分到大于区域
swap(aar, --more, l);
} else {
//相等时,仅仅移动l
l++;
}
}
return new int[]{less + 1, more - 1};
}
复杂度分析
时间复杂度O(N*logN),额外空间复杂度O(logN)
堆排序
相关概念【更多相关概念参考通俗易懂,什么是二叉堆?】
最大堆
根结点的值是所有堆结点值中最大者,且每个结点的值都比其孩子的值大。
最小堆
根结点的值是所有堆结点值中最小者,且每个结点的值都比其孩子的值小。
思路
- 将数组数据排列成最大二叉堆结构,保证数组下标0位置为数组最大元素;
- 交换数组下标0与length-1位置元素,即把最大数下沉到末尾位置;
- 去除末尾位置元素,对剩余数组元素找最大值,交换到0位置;
- 不断进行步骤2和步骤3,最终完成整体数组排序;
代码实现
/**
* 堆排序
*
* @param arr 需要排序的目标数组
*/
private static void heapSort(int[] arr) {
if (arr == null || arr.length < 2) {
return;
}
for (int i = 0; i < arr.length; i++) {
//构造一个最大堆
heapInsert(arr, i);
}
int size = arr.length;
swap(arr, 0, --size);
while (size > 0) {
//将最大值交换到0位置
heapify(arr, 0, size);
//下沉最大值到末尾
swap(arr, 0, --size);
}
}
private static void heapify(int[] arr, int index, int size) {
//index节点对应的左孩子节点
int left = index * 2 + 1;
//只要left节点没有越界
while (left < size) {
//index节点对应的右孩子节点
int right = left + 1;
//右孩子节点 如果没越界 取左右节点最大的下标即为maxIndex
int maxIndex = right < size && arr[right] > arr[left] ? right : left;
//再找出maxIndex下标和index下标的最大值对应的下标
maxIndex = arr[maxIndex] > arr[index] ? maxIndex : index;
//如果就是index节点,则退出while循环
if (maxIndex == index) {
break;
}
//交换index和maxIndex下标的值
swap(arr, index, maxIndex);
//将maxIndex赋值给index,继续往下找符合条件的下标进行交换
index = maxIndex;
left = index * 2 + 1;
}
}
/**
* 基于传入的数组构造一个最大堆
*
* @param arr
* @param index 插入堆的数组下标
*/
private static void heapInsert(int[] arr, int index) {
//只要i位置元素大于它的根节点,就交换他两的位置,直到根节点(即数组下标为0位置)为最大值
while (arr[index] > arr[(index - 1) / 2]) {
swap(arr, index, (index - 1) / 2);
index = (index - 1) / 2;
}
}
复杂度分析
时间复杂度O(N*logN),额外空间复杂度O(1)
算法案例
逆序对问题
题目描述
剑指 Offer 51. 数组中的逆序对;
在数组中的两个数字,如果前面一个数字大于后面的数字,则这两个数字组成一个逆序对。输入一个数组,求出这个数组中的逆序对的总数。
示例 1:
输入: [7,5,6,4]
输出: 5
根据归并排序思路:
public static int reversePairs(int[] nums) {
if (nums == null || nums.length < 2) {
return 0;
}
return reversePairs(nums, 0, nums.length - 1);
}
private static int reversePairs(int[] nums, int l, int r) {
if (l == r) {
return 0;
}
int mid = l + (r - l) / 2;
return reversePairs(nums, l, mid) + reversePairs(nums, mid + 1, r) + mergeReversePairs(nums, l, mid, r);
}
private static int mergeReversePairs(int[] nums, int l, int mid, int r) {
int i = 0;
int[] help = new int[r - l + 1];
int p1 = l;
int p2 = mid + 1;
int res = 0;
while (p1 <= mid && p2 <= r) {
if (nums[p1] > nums[p2]) { //如果左部分比右部分大,则逆序对数量应加上p2到右部分总长度
res += r - p2 + 1;
help[i++] = nums[p1++];
} else {
help[i++] = nums[p2++];
}
}
while (p1 <= mid) {
help[i++] = nums[p1++];
}
while (p2 <= r) {
help[i++] = nums[p2++];
}
for (i = 0; i < help.length; i++) {
nums[l + i] = help[i];
}
return res;
}
结语
如果以上文章对您有一点点帮助,希望您不要吝啬的点个赞加个关注,您每一次小小的举动都是我坚持写作的不懈动力!ღ( ´・ᴗ・` )