带着排序学时间/空间复杂度
排序和时间复杂度
- 带着排序学时间/空间复杂度
- 冒泡排序
- 选择排序
- 选择排序法2原理:
- 插入排序
- 希尔排序(缩小增量排序)
- 堆排序
- 快速排序
- 归并排序
- 不基于比较的排序
- 计数排序
- 桶排序
- 稳定性
时间复杂度是打开数据结构大门的第一步,学会计算时间复杂度对我们编写程序提高程序运行效率有很大的帮助,如何计算时间复杂度,时间复杂度指的是一个变量被使用了多少次,统计使用次数。我们一般使用大O阶方法,随着问题规模的增大,其中n与f(n)增长速度基本相同。就拿我们最常用的选择排序,他的时间复杂度如何计算呢?
首先我们需要了解如何判定一段代码的时间复杂度:
一般我们计算时间复杂度都是以最坏的情况去考虑。
使用常数 1 来取代运行时间中所有的加分常数。
计算时间复杂度需要将个位数去除,只保留最高系数项,其他的数并去除。
如果去除其他系数后,最高项存在且不是1,则去除这个项目相乘的系数。
以上三步得到的就是大O阶的时间复杂度。
要认识空间复杂度,学习其计算方法,首先要明白空间复杂度到底是什么:空间复杂度是考虑程序运行时占用内存的大小,考虑在程序运行时产生对内存的影响,就像C语言计算的内存对齐,是一种内存的体现,而在计算过程中,也需要考虑到空间复杂度不能太大,对运行速率会有影响。
1.对一个算法在运行过程中占用内存空间大小的量度,记做S(n)=O(f(n))
2.空间复杂度只是一个预估的大小
冒泡排序
在初学阶段,最常使用的排序方法就是冒泡排序
冒泡排序基本原理:
- 将最大/最小的数据先进行排序
- 依次减小比较的数据量
- 直到数据有序
代码块
public static void BubblingSort(int[] array){
for (int i = 0; i < array.length-1; i++) {
for (int j = 0; j < array.length-i-1; j++) {
if(array[j]>array[j+1]){
int temp = array[j];
array[j] = array[j+1];
array[j+1] = temp;
}
}
}
}
计算时间复杂度:
需要计算数据被使用了几次,两个嵌套循环语句都循环,外侧循环n-1次,内侧循环n-i次,计算时间复杂度(n-1)
*
(n-i)>n^2-in+i-n
将1来取代所有的加分常数》n^2-2n+1
去除常数项,去除其他系数项,留下最高系数项,这里的 i 是常数项==》n^2
去除相乘系数项==》n^2
这样可以得到冒泡排序的时间复杂度:O(N^2)
接下来计算空间复杂度:
统计程序中创建的变量,只有i和j,并没有其他变量,所以得到空间复杂度:O(1)
在接下来的排序中经常会使用到交换两个数据,这里我单独将其提炼出来,以防大家看不懂
private static void swap(int[] array, int i, int j) {
int temp = array[i];
array[i] = array[j];
array[j] = temp;
}
选择排序
插入排序的基本原理(* ̄︶ ̄)
- 保存需要插入的下标位置
- 便利下标以后的位置,找到最小下标位置
- 与第一步保存的下标,两个元素进行交换】
代码块
public static void SelectSort(int[] array){
for (int i = 0; i < array.length; i++) {
int min = i;
for (int j = i+1; j < array.length; j++) {
min=array[j]<array[min]?j:min;
}
swap(array,i,min);
}
}
选择排序法2原理:
- 使用两个数字代表最大值,最小值
- 遍历数组,将两个值找到
- 将最大值放在最右边,最小值放在最左边,以此类推
注意:这里交换时可能会把下标的值改掉,所以这里如果最大值与最左边的相等时,就将最小值的下表赋值给最小值,就是提前把值进行交换。
public static void SelectSort2(int[] array){
int left = 0;
int right = array.length-1;
while (left < right){
int maxIndex = left;
int minIndex = left;
for (int i = left+1; i <= right; i++) {
maxIndex = array[i]>array[maxIndex]?i:maxIndex;
minIndex = array[i]<array[minIndex]?i:minIndex;
}
//这里可能将最大值换成minIndex的位置
//言外之意就是,最大值位于left
if(left == maxIndex){
maxIndex = minIndex;
}
swap(array,left,minIndex);
swap(array,right,maxIndex);
left++;
right--;
}
}
时间复杂度
同样的,这里也使用了两个嵌套循环语句。外层循环n次,内层循环(n-1+n-2+n-3+……+n-i),由此可见是一个等差数列。带入公式=》sn=[n(1+n)]/2 ->(n-i+n-1)
*
(n)/2=2n^2 -2i - 2/2 =n^2 -i - i=n2->O(N2)
求出时间复杂度为:O(N^2)
计算空间复杂度:
这里也没有创建多余的其他变量,所以得到空间复杂度:O(1)
插入排序
插入排序基本原理
- 将当前元素记录为 temp
- 从当前元素的前一个元素进行遍历往前遍历 记录当前为 i ,则前一个元素 是i-1,将 j 设置为 i-1
- 如果当前元素比较后一个元素大,则进行赋值操作,否则退出循环 当前 temp 的元素比 j 小就将 j 的值赋值给 j+1;相当于将数据往后移动。
- 最后将刚刚开始保存的值填入j下标
public static void InsertSort(int[] array){
for (int i = 1; i < array.length; i++) {
int temp = array[i];
int j = i-1;
for (; j >=0 ; j--) {
if(temp < array[j]){
array[j+1] = array[j];
}else {
break;
}
}
array[j+1] = temp;
}
}
计算时间复杂度:
这里我们讨论最坏情况:两个嵌套循环中外层循环:n-1次 内层循环:1+2+3+……+n-1=>等差数列求和公式=(1+n)
*
(n-1)/2=(n^2 + 1 - 2n)/2,去除多余的部分得到时间复杂度:O(N^2)
空间复杂度:
每次进入循环会创建一个temp ,多次使用内存,使用了n-1次,所以得到空间复杂度:O(N)
希尔排序(缩小增量排序)
希尔排序原理:
- 将数组元素进行分组比较,分组元素gap个
- 使用插入排序进行排序
- 扩大分组元素进行比较
- 继续使用插入排序将分好的组进行元素
- 直到将组值元素扩大到整个数组
public static void ShellSort(int[] array){
int gap = array.length;
while (gap > 1){
//调用额外方法
gap /= 2;
shellSort(array,gap);
}
//整体进行排序
shellSort(array,1);
}
//下面的排序,相当于改写了插入排序
private static void shellSort(int[] array,int gap){
for (int i = gap; i < array.length; i++) {
int temp = array[i];
int j = i-gap;
for (; j >= 0 ; j-= gap) {
if(array[j] > temp){
array[j+gap] = array[j];
}else {
break;
}
}
array[j+gap] = temp;
}
}
时间复杂度:科学家提出的,经过大量运算得到希尔排序的时间复杂度,O(n1.25)~O(1.6*n1.25)
空间复杂度:O(1)
堆排序
大根堆排序原理(向下调整):
- 比较子节点的最大值,得到子节点最大值
- 将子节点与父节点对比,如果子节点大于父节点,就进行交换
- 把子节点当成父节点进行下一轮比较
public static void heapSort(int[] array){
createBigHeap(array);//创建一个大根堆
int end = array.length-1;
while (end>=0){
swap(array,end,0);
shiftDown(array,end,0);
end--;
}
}
private static void createBigHeap(int[] array){
for (int i = (array.length-1)/2; i >=0 ; --i) {
shiftDown(array,array.length,i);
}
}
public static void shiftDown(int[] array,int len,int parent){
int child = parent*2+1;
while (child<len){
if(child+1<len && array[child] < array[child+1]){
child++;
}
if(array[child]>array[parent]){
swap(array,child,parent);
parent = child;
child = parent*2+1;
}else {
break;
}
}
}
时间复杂度:
这里每次只需要比较单个节点值的大小,时间复杂度为logN,每次都需要循环n次,得到时间复杂度 n
*
logn,得到大O阶表示法O(NlogN)
空间复杂度:
该排序没有使用额外的空间所以得到的空间复杂度:O(1)
快速排序
快速排序原理:
- 在数组中取一个数字作为基准值
- 声明两个变量,右边找到比基准值小的就停下交换,左边类似
- 交换两个下标的值,循环进行,直到退出
- 此时两个变量相遇,将基准值和相遇的下标的值进行交换
public static void QuickSort(int[] array){
quickSort(array,0,array.length-1);
}
private static void quickSort(int[] array,int start,int end){
//递归退出条件
if(end - start <= 0){
return;
}
//优化,目的是为了减少递归,可随机数字,建议不要太小
if(end - start <= 14){
//插入排序
InsertSort2(array,start,end);
return;
}
int index = midThree(array,start,end);//基准
//将基准值与开始值进行交换,因为一般会将第一个数字设置为基准值
swap(array,index,start);
int value = partition(array,start,end);//划分
quickSort(array,start,value-1);
quickSort(array,value+1,end);
}
//改写的插入排序
public static void InsertSort2(int[] array,int left,int right){
for (int i = left; i <=right ; i++) {
int temp = array[i];
int j = i-1;
for (; j >=0 ; j--) {
if(temp < array[j]){
array[j+1] = array[j];
}else {
break;
}
}
array[j+1] = temp;
}
}
//交换
public static int partition(int[] array,int left,int right){
int temp = array[left];
while (left<right){
//加等号,遇到一样的数据跳过
while (left < right && array[right] >= temp){
right--;
}
array[left] = array[right];
while ( left < right && array[left] <= temp){
left++;
}
array[right] = array[left];
}
array[left] = temp;
return left;
}
//快排的优化+均匀分割1.随机选取基准法 2.三树取中法
private static int midThree(int[] array,int left,int right){
int min = array.length/2;
if(array[left]<array[right]){
if(array[min]<array[left]){
return left;
}else if(array[min]>array[right]){
return right;
}else {
return min;
}
}else {
//array[left]>array[right]
if(array[min] > array[left]){
return left;
}else if(array[min] < array[right]){
return right;
}else {
return min;
}
}
}
快速排序Hoare法
- 在数组中取一个数字作为基准值
- 声明两个变量,左边找到比基准大的值停下来,右边找到比基准大的停下来
- 交换两个下标的值,循环进行,直到退出
- 此时两个变量相遇,将基准值和相遇的下标的值进行交换
public static int quick2(int[] array,int left,int right){
int temp = array[left];
int i = left;//记录下标
while (left<right){
while (left <right && array[right]>=temp){
right--;
}
while (left<right && array[left] <= temp){
left++;
}
swap(array,left,right);
}
swap(array,left,i);//将基准移过去
return left;
}
时间复杂度:
每次将数组分成两部分进行排序,所以得到logn,递归的时间复杂度为n,所以得到总的时间复杂度为
(n*logN)
空间复杂度:
这里使用了递归,所以会消耗一定的空间,因为基准值的不同所以时间复杂度会有一定的波动:
O(logN) ~ O(N)
不使用递归的快速排序
原理:
1.准备一个栈,用于存放基准的左边和右边
2.先将基准两边数组开始下标与结束下标依次入栈
3.在栈中取需要排序的数组长度,然后比较,再继续下一步
4.如果栈为空,说明已经排序成功了
public static void QuickSort2(int[] array){
Deque<Integer> stack = new LinkedList<>();
int left = 0;
int right = array.length-1;
int pivot = partition(array,left,right);
if(pivot > left+1){
stack.push(left);
stack.push(pivot-1);
}
if(pivot < right-1){
stack.push(pivot+1);
stack.push(right);
}
while (!stack.isEmpty()){
right = stack.poll();
left = stack.poll();
pivot = partition(array,left,right);
if(pivot >left+1){
stack.push(left);
stack.push(left+1);
}
if(pivot < right-1){
stack.push(pivot+1);
stack.push(right);
}
}
}
时间复杂度与空间复杂度与递归的快速排序一致。
归并排序
归并排序原理
1.取中间值将数组分为两组
2.将分好的数组继续向下分裂,直到数组长度为1
3.将两个相邻的数组进行排序
4.直到整合完所有数组
/**
* 归并排序
* 时间复杂度:O(n*logn)
* 空间复杂度:O(N)
* 稳定
* @param array
*/
public static void mergeSort(int[] array){
mergeSort(array,0,array.length-1);
}
private static void mergeSort(int[] array ,int left,int right){
if(left >= right){
return;
}
int mid = left+(right-left)/2;
mergeSort(array,left,mid);
mergeSort(array,mid+1,right);
mergeSort(array,left,right,mid);
}
private static void mergeSort(int[] array, int start, int end, int mid) {
int s1 = start;
int s2 = mid+1;
int[] tmp = new int[end - start+1];
int k =0;
while (s1 <= mid && s2 <= end){
if(array[s1] <= array[s2]){
tmp[k++] = array[s1++];
}else {
tmp[k++]=array[s2++];
}
}
while (s1 <= mid){
tmp[k++] = array[s1++];
}
while (s2<=end){
tmp[k++] = array[s2++];
}
for (int i = 0; i < tmp.length ; i++) {
array[i+start] = tmp[i];
}
}
时间复杂度:
归并排序中,每次将数组分成两个,所以构成以2为底的logN次分割,然后又会分割N次,所以计算得到时间复杂度:O(NlogN)
空间复杂度:
在递归的过程中,不断的创建变量,一共递归N次,所以空间复杂度为:O(N)
非递归实现归并排序
思想:
1.每一个数字对于自身是有序的,先将gap设置为1
2.然后增加gap的量,不过要控制gap不能超过数组长度
3.使用left,right,mid分成两组数据一起排序,数组中数字相对有序
4.在进行下一组,直到gap等于数组长度
//归并排序的非递归实现
public static void mergeSort2(int[] array){
int gap = 1;
while (gap<array.length){
for (int i = 0; i <array.length ; i+=gap*2) {
int left = i;
int mid = left+gap-1;
//防止越界,下面也是如此
if(mid>=array.length){
mid = array.length-1;
}
int right = mid+gap;
if(right>=array.length){
right = array.length-1;
}
//调用排序范围的排序方法
mergeSort(array,left,right,mid);
}
gap*=2;
}
}
以上就是排序与时间复杂度和空间复杂度的学习,对于编写代码的时间和空间控制是需要自己有个底的,中间不乏有一些人说:牺牲空间,换取时间,或者牺牲时间,换取空间。
不基于比较的排序
计数排序
计数排序思想:
1.将数组中最大值和最小值分别提出来
2.固定数组长度
3.将下标和数字结合,找到数字所处的位置,并作为计数记录下来
如图:
接下来只需要将count数组进行打印/放回之前数组即可。
//虽然以上是针对0~9之间的数据,不过该排序也是针对变化幅度不大的数据进行排序,主要是看数据最大和最小值来确定计数数组的大小
public static void countSort(int[] array){
//1.遍历数组找到最大值与最小值
int max = array[0];
int min = array[0];
for (int i = 0; i <array.length ; i++) {
max = max>array[i]?max:array[i];
min = min<array[i]?min:array[i];
}
//根据范围定义计数数组的长度
int len = max-min+1;
int[] count = new int[len];
for (int i = 0; i <array.length ; i++) {
count[array[i]-min]++;
}
//遍历计数数组
int index = 0;
for (int i = 0; i < count.length; i++) {
while (count[i]>0){
array[index] = i+min;
index++;
count[i]--;
}
}
}
计数排序的时间复杂度:
首先遍历了一次数组找到最大值与最小值(n),接下来遍历一次数组将数字计数(n),然后遍历了一次计数数组将值填入数组之中(max-min+1)。
所以计算得到时间复杂度:O(N)+(范围)
空间复杂度:
除了数组长度,额外申请一个计数数组长度为(max - min+1)
得到空间复杂度:(范围)
桶排序
桶排序原理:
1.申请一个10个队列
2.将数组最低位(个位)按照队列下标依次入队
3.从0~9的顺序从前往后出队列放入数组
4.按第二低位(十位)按照队列下标依次入队
5.和第二步一样,直到放完数组中的最高树的位,依次出队,则排序完成。
如图:
/**
* 桶排序(基数排序)
* 时间复杂度:O(N)+(范围)
* 空间复杂度:O(范围)
* 不稳定
* @param array
*/
public static void countSort2(int[] array) {
int maxDigit = getMaxDigit(array);
radixSort(array,maxDigit);
}
private static void radixSort(int[] array, int maxDigit) {
int mod = 10 ;
int dev = 1;
for (int i = 0; i <maxDigit ; i++,dev *=10,mod *=10) {
int[][] counter = new int[mod*2][0];
for (int j = 0; j < array.length; j++) {
int bucket = ((array[j]%mod)/dev)+mod;
counter[bucket] = arrayAppend(counter[bucket],array[j]);
}
int pos = 0;
for(int[] bucket:counter){
for (int value:bucket) {
array[pos++] = value;
}
}
}
}
private static int[] arrayAppend(int[] array, int value) {
array = Arrays.copyOf(array,array.length+1);
array[array.length-1] = value;
return array;
}
//获取最高位数
private static int getMaxDigit(int[] array) {
int maxValue = array[0];
for (int num:array) {
maxValue = maxValue>num?maxValue:num;
}
int length = 0;
for (int i = maxValue; i != 0 ; i/=10) {
length++;
}
return length;
}
}
时间复杂度:
遍历数组找到最大的数字N次,遍历数组记录数字N次。得到时间复杂度为O(N)
空间复杂度:
申请了N个空间来存放数组的元素,空间复杂度为O(N)
稳定性
当然,排序中不仅有时间复杂度和空间复杂度的一些特性,还有稳定性,稳定性是一个排序中较为重要的一部分。
稳定性:指一个排序中,如果遇到两个相同的元素时,在排序后与排序前,他们的相对位置不变,则代表这个排序是稳定的,反之,则不稳定。
在以上的排序中,只有3个排序是稳定的:冒泡排序,插入排序,归并排序。