JAVA-数据结构与算法-复杂度和排序算法

435 阅读6分钟

写在前面

复杂度

时间复杂度

  • 本质上就是函数的增长趋势,避免指数阶的算法
  • 时间频度,T(n),出一次结果,所操作的循环次数,一个for循环为n,两个并列的for循环为2n,两个嵌套的for循环为n^2,忽略常数项、低次项(数量级足够大)、系数(次方可以,次方以上不行)
  • 时间复杂度,O(n)省略T(n)的常数项、低次项、系数

类型

  • 常数阶,无论执行了多少行,只要没有循环,就是O(1),不会随着变量的增长而增长
  • 对数阶,log2n2^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; j < arr.length - 1; j++) {
    for (int i = 0; i < arr.length - j - 1; i++) {
        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; j < arr.length - 1; j++) {
    for (int i = 0; i < arr.length - j - 1; i++) {
        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 < arr.length - 1; i++) {
    //假设第i个最小,每次都从第i开始假设,并每一轮把最小的排在前面
    index = i;
    min = arr[i];
    //i=0时,与后面的所有进行比较,已经是最小的了,放在最前面
    //从第i开始,每轮比较出第i个最小的,放在前面
    for (int j = 0 + i; j < arr.length; j++) {
        //如果最小值不是这个,保留新的最小值
        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; i < arr.length; i++) {
    insertVal = arr[i];
    insertIndex = i - 1; //即arr[n]临近的前一个
    //带插入的数还没找到位置,就需要将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/21组,进行插入排序 image.png

交换法

  • 举例分析
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; i < arr.length; i++) {
    //遍历各组中所有元素(共五组,每组有两个元素)
    for (int j = i - 5; j >= 0; j-= 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; i < arr.length; i++) {
    //遍历各组中所有元素(共2组,每组有5个元素)
    for (int j = i - 2; j >= 0; j-= 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; i < arr.length; i++) {
    //遍历各组中所有元素(共2组,每组有5个元素)
    for (int j = i - 1; j >= 0; j-= 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; gap > 0; gap /=  2) {
    for (int i = gap; i < arr.length; i++) {
        for (int j = i - gap; j >= 0; j-= 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; gap > 0; gap /=  2) {
    for (int i = gap; i < arr.length; i++) {
        //插入
        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
  • 利用归并的思想实现,采用经典的分治策略,将问题成一些小的问题,然后递归求解,而的阶段,将的阶段得到的各答案放在一起
  • 分+合的方法
//8, 4, 5, 7, 1, 3, 6, 2
public static void mergeSort(int[] arr, int left, int right, int[] temp) {
    if (left < right) {
        int mid = (left + right) / 2;
        /*左 当传入两个相邻数的时候,例如0 1时,mid=0,
         此时,尝试进入下一次递归,0进入左,1进入右,
         会直接跳过下一层递归的`left < right`的if语句,进入合并*/
        mergeSort(arr, left, mid, temp);
        //右 
        //偶数数组8,中间索引为3,右边是从4开始;
        //基数数组7,中间索引为3,右边是从4开始
        //因此右边起始为mid+1
        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索引的当前数组
    //先把左右两边有序的数据按照规则填充到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; j < arr.length; j++) {
    //取出个位
    int digitOfElement = arr[j] % 10;
    //放入到对应的桶中 放入第几`digitOfElement`个桶的第几个位置`bucketElementCounts[digitOfElement]`
    bucket[digitOfElement][bucketElementCounts[digitOfElement]] = arr[j];
    //每个桶的个数
    bucketElementCounts[digitOfElement]++;
}
//按照这个桶的顺序,取出顺序,放入原来的数组
int index = 0;
for (int k = 0; k < bucketElementCounts.length; k++) {
    //循环第k个桶
    //如果桶中有数据,放入原数组
    if (bucketElementCounts[k] != 0) {
        for (int l = 0; l < bucketElementCounts[k]; l++) {
            //循环k桶中元素的个数
            //取出元素放入到arr
            arr[index] = bucket[k][l];
            //放入原数组的索引++
            index ++;
        }
    }
    //每一轮轮处理后,需要把每个桶的数量清零 bucketElementCounts[k]
    bucketElementCounts[k] = 0;
}
  • 第二轮,放十位桶
for (int j = 0; j < arr.length; j++) {
    //取出个位
    int digitOfElement = arr[j] / 10 % 10; // 748 / 10 = 74 % 10 = 4
    //放入到对应的桶中 放入第几`digitOfElement`个桶的第几个位置`bucketElementCounts[digitOfElement]`
    bucket[digitOfElement][bucketElementCounts[digitOfElement]] = arr[j];
    //每个桶的个数
    bucketElementCounts[digitOfElement]++;
}
//按照这个桶的顺序,取出顺序,放入原来的数组
index = 0;
for (int k = 0; k < bucketElementCounts.length; k++) {
    //循环第k个桶
    //如果桶中有数据,放入原数组
    if (bucketElementCounts[k] != 0) {
        for (int l = 0; l < bucketElementCounts[k]; l++) {
            //循环k桶中元素的个数
            //取出元素放入到arr
            arr[index] = bucket[k][l];
            //放入原数组的索引++
            index ++;
        }
    }
}

完全代码

  • bucketElementCounts数组决定了第k个桶存的数量,当需要重新将数组复制到原数组时,bucket[k]桶的循环次数bucketElementCounts[k]决定,每一轮过后都清零
//得到数组中最大的数的位数
int max = arr[0];
for (int i = 0; i < arr.length; i++) {
    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; i < maxLength; i++, n *= 10) {
    //从个位开始,依次进行排序
    for (int j = 0; j < arr.length; j++) {
        //取出个位
        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 < bucket.length; k++) {
        //循环第k个桶
        //如果桶中有数据,放入原数组,这里用记录的桶的数据个数作为判断
        if (bucketElementCounts[k] != 0) {
            for (int l = 0; l < bucketElementCounts[k]; l++) {
                //循环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; i < negativeArr.length / 2; i++) {
    //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; i < negativeArr.length; i++) {
    originArr[i] = negativeArr[i];
}
for (int i = 0; i < postiveArr.length; i++) {
    originArr[negativeArr.length + i] = postiveArr[i];
}
  • 第二个思路,加上一个特别大的值处理完后还原
//加上特别大的数
for (int i = 0; i < arr.length; i++) {
    arr[i] = arr[i] + 1000;
}
//排序
radixSort(arr);
//还原
for (int i = 0; i < arr.length; i++) {
    arr[i] = arr[i] - 1000;
}