算法周练一

92 阅读8分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

选择排序

思路

选择排序的初衷就是把一个数组的最小值放到第一位,然后次小值放到第二位,再次小值放到第三位。

从中我们可以看到,我们首先需要一个循环来决定现在选择的数是第几小,然后再来一个循环来找到那个值。

举例

3,5,8,0,6,4,2

上面这个数组就是举例的目标

我们第一步找他们中最小的,我们循环一遍数组之后发现是0,那我们就让0和第一位的3交换。

0,5,8,3,6,4,2

这个时候数组就变成了这个样子,那我们第二步是从哪开始呢?还是0吗?显然不是,因为0已经到了它该到的位置。

之后我们从第二位开始循环寻找第二小的数字。我们找到了最后的2,那我们这个时候就可以把最后的2和第二位的5交换,

循环上面的步骤就可以把数组排好了。

代码展示

public static void selectionSort(int[] arr){
    //这个循环就是来控制找的是第几小的数字。i=0的时候就表示找的是第0小的值,i=1的时候就表示找的是第1小的值。
    for(int i = 0;i<arr.length;i++){    
        int min = i;
        //这个循环就是来控制寻找的,我们要在还没有处理的那部分里面找到那个我们第一层循环需要找到的值。
        //注意这边j的开始不是0,是i + 1。
        for(int j = i + 1;j< arr.length;j++){
            if(arr[min] > arr[j]){
                min = j;
            }
        }
        //交换,把较小的值放在前面的地方。
        int t = arr[min];
        arr[min] = arr[i];
        arr[i] = t;
    }
}

大家可以根据上面的代码还有注释可以了解到选择排序的运行过程。

冒泡排序

思路

冒泡排序的话,大家可以想象一下泡泡,泡泡从底部到水面是越来越大的,这个是跟压强是有关系的,具体的这边也不细说了。

那么我们可以记住这个来想出一个排序方法,就是让最前面的那个数字慢慢往后面飘。

举例

就比如说这个数组

3,5,8,0,6,4,9

我们先让第一位的3和后面那个5比较,我们发现比不过,那么我们就跳到下一个。

之后就是第二位的5和第三位的8比较,发现也是比不过,那我们也是跳到下一个,

这个时候我们发现第三位的8是比第四位的0大的,那我们这个时候怎么办呢?

那我们就把8这个泡泡往后面飘,就是让0和8交换。数组就变成了下面这个样子

3,5,0,8,6,4,9

然后我们比较第四位的8和第五位的6,发现是可以比过的,这个时候就再交换。

3,5,0,6,8,4,9

之后像上面一样,8最后会停在9的前面,那我们一趟的循环就结束了

3,5,0,6,4,8,9

这一趟循环之后,我们继续从第一位开始循环,但是结束的位置就有变化了,结束的位置就从数组的长度-1到了数组的长度-2

这个是为什么呢?

原因就是当我们排好一个位置之后,最大的位置总是排好的,既然是排好的,那我们还需要再去排吗?显示是不用的。

代码

public static void bubbleSort(int[] arr){
    //第一层的循环是用来控制循环的次数的。
    for(int i = 0;i<arr.length;i++){
        //第二层的循环是用来比较的,也就是泡泡的上浮。
        //需要注意的是,j是小于arr.length - 1 - i这边对减的1,是防止下面if判断语句里面的arr[j+1]越界。
        for(int j = 0;j<arr.length - 1 - i;j++){
            if(arr[j] > arr[j+1]){
                //如果这个数,比的过后面的泡泡,那么就往后面飘
                int t = arr[j];
                arr[j] = arr[j + 1];
                arr[j + 1] = t;
            }
        }
    }
}

插入排序

思路

插入排序就和打扑克一样,就是把你手中拿到的牌和牌堆里面的牌比较,放到它应该在的地方。

举例

3,5,0,8,6,4,9

假设你有这些牌,然后你拿到了第一张牌【3】,因为你之前还没有拿到过牌,所以3就自然有序了。

之后你从牌库里面拿了5,先放在你有牌堆的后面,也就是【3,5】,你发现5比3大,自然就是在3的后面

之后你继续拿牌,这次你拿到了0,你先把0放在最后面,【3,5,0】,然后0和5比较,0小,那么就交换【3,0,5】,然后发现3也比0小,这个时候继续交换【0,3,5】。这个时候你手里的牌就有序了。

从上面这一点的操作你就可以发现,交换产生的原因是你后面拿到的牌比你之前拿到的牌要小。

那么了解了这个,我们就可以看代码了。

代码

public static void insertionSort(int[] arr){
    //这个第一层循环就是你拿牌的次数。
    for(int i = 1;i<arr.length;i++){
        int end = arr[i];   //这个是记录你刚拿到的牌
        int begin = i - 1;  //这个是你要从哪开始比较
        while(begin >= 0){  //这个循环就是比较的过程
            if(arr[begin] > end){  //这个就是说,如果你拿到的牌比较小,这个时候就要交换。
                swap(arr , begin , begin + 1);
                begin--;
            }else{   //如果没有比这个牌小的牌出现,那么就退出循环。
                break;
            }
        }
    }
}
public static void swap(int[] arr , int i , int j){
    int t = arr[i];
    arr[i] = arr[j];
    arr[j] = t;
}

还有一种写法是填坑的写法

public static void insertionSort(int[] arr){
    for(int i = 1;i<arr.length;i++){
        int end = arr[i];
        int begin = i - 1;
        while(begin >= 0){
            if(arr[begin] > end){
                arr[begin + 1] = arr[begin];
                begin--;
            }else{
                break;
            }
        }
        arr[begin + 1] = end;
    }
}

这个比上面好的就是少了交换的步骤,常数时间比较快。

二分法

思路

二分就是在一个数组里面找数字的。如果我们用遍历的话,就是O(n)的时间复杂度。那么二分是怎么来简化的呢?就是通过找中间的数字和你要找的数字的大小关系来确定找的地方的。

举例

1,2,3,4,5,6,7,8,9

首先我们要明白的是。二分查找只能在有序数组里面找,但是有些题目虽然是无序的,但是依旧可以利用二分的思想来解题。

那我们看上面的那个数组,我们假设我们要找的数字是2,那我们这个时候是知道这个数组的长度是9,那么我们也知道这个数组的left是0,right是8,那么我们第一个找的地方就是(left+(right- left)/2) = 4.

为什么我们会用(left+(right- left)/2)这个来算中间值呢?

这个是怕left + right会越界的问题,这样子比较安全。

那么我们已经知道了中间的那个数字是arr[4] = 5;

比较5和我们要找的2,发现2是小于5的

这个时候我们就可以缩小范围了,数字就是从0到3这个区间。

这样我们可以做到,每次都找一半来找数字,时间复杂度就下来了。变成了O(log2N)

代码

public static int find(int[] sortedArr, int num){
    if (sortedArr == null || sortedArr.length == 0) {
        return -1;
    }
    int left = 0;
    int right = sortedArr.length - 1;
    int mid = (left + (right - left)/2);
    while(left <= right){
        if(sortedArr[mid] > num){
            right = mid - 1;
        }else if(sortedArr[mid] < num){
            left = mid + 1;
        }else{
            return mid;
        }
        mid = (left + (right - left)/2);
    }
    return -1;
}

在有序数组中利用二分法找到大于等于num最左的数字

思路

这个题目就是利用了二分,不过之前的二分是找到之后就返回,这个不是,这个是找到之后还会再去找。直到最后一步

举例

1,2,2,2,3,3,3,4,5,6

我们可以看看上面的数组。假设我们要找的num是3。

那我们看看我们知道的有什么,left = 0;right = 9;那为我们可以推出mid = 4

我们看看arr[4]是多少?

正好是3,那普通的时候,我们这个时候就结束了,但这次是不一样的,我们这次要继续二分,我们看看这个数字是否小于3,明显这个是符合的,然后让我们的left变成mid + 1;

我们继续看看我们的值都变成了什么,left = 5 , right= 9, mid = 7 那我们看看arr[7]是多少?是4。

那我们看看这个数字是不是小于3,明显不符合,那我们这个时候就需要一个index变量来记录这个mid了。然后让right变成mid-1。这样就可以了。

之后就上上面的循环

代码

public static int nearestIndex(int[] arr, int value) {
    int left = 0;
    int right = arr.length - 1;
    int index = -1; // 记录最左的对号
    while (left <= right) { // 至少一个数的时候
        int mid = left + ((right - left) >> 1);
        if (arr[mid] >= value) {
            index = mid;
            right = mid - 1;
        } else {
            left = mid + 1;
        }
    }
    return index;
}

利用二分法的思想解决局部最小值

思想

我们先来了解一下这道题目是怎么样的。

这道题目的数组是可以无序的,但是数字与数字之间是不相等的。 那什么是局部最小呢?

3,2,1,4,5

这个数组的1 就是局部最小,因为1比左边小,比右边小,这样这个就是局部最下的了。

那我们怎么利用二分呢?

我们可以知道,一个数组,因为左右不相等,那么数组肯定是线性上升或者线性下降。如果你的left是局部下降的,right是局部上升的,那么left和right之间必定有一个局部最小出现。

这个正好是和二分法的思想是相对应的。

举例

3,2,1,4,5 我们来看看我们知道的是什么 left = 0;right = 4。

我们比较一下left和left + 1的大小,可以很容易知道,left这边的线性是下降的。

我们比较一下right和right - 1的大小,可以很容易知道,right这边的线性是上升的。

一个数组的头是下降的,尾巴是上扬的,那么中间必定有一个局部最小的地方。我们只要发现一个就可以了。

我们计算mid = 2;

我们看看mid的周围是上升还是下降。

如果mid 小于mid + 1还有mid - 1,那么这个就是局部最小。 还有其他的可能性,咱们可以看代码来解答。

代码

public static int getLessIndex(int[] arr){
    if (arr == null || arr.length == 0) {
        return -1;
    }
    if(arr.length == 1){
        return 0;
    }
    if(arr[0] < arr[1]){    //这个判断是为了判断开头是否是局部最小,如果是就直接返回
        return 0;
    }
    //这个判断是为了判断结尾是否是局部最小,如果是就直接返回
    if(arr[arr.length - 1] <arr[arr.length - 2]){
        return arr.length - 1;
    }
    int left = 0;
    int right = arr.length - 1;
    int mid = (left +(right - left)/2);
    while(left <= right){
        //这个就是局部最小判断的条件,中间的要比两边的都小。
        if(arr[mid] < arr[mid + 1] && arr[mid] < arr[mid - 1]){
            return mid;
        }else if(arr[mid] > arr[mid + 1]){
            //如果比右边的大,说明这个线性是下降的,既然是下降的,那么局部最小就在mid到right中间。
            left = mid + 1;
        }else{
            //如果比左边的大,说明这个线性是上升的,既然是上升的,那么局部最小就在left到mid中间。
            right = mid - 1;
        }
        mid = (left +(right - left)/2);
    }
    return -1;
}