左神算法学习笔记

633 阅读7分钟

1.算法认识复杂度对数期二分法与异或运算

  • 数组的查找是O(1),因为他是直接计算地址偏移量

  • 左移<< :num << n, 相当于 num 乘以2的 n 次方

  • 有符号右移>> : 左边补上符号位 (负的就填1 正的就填0)。num >> n, 相当于 num 除以2的 n 次方

  • 无符号右移>>> : 空位都补零

  • 异或运算:记为混略进位两数相加。 110 ^ 111 = 001

    • 性质1:0 ^ 0 = 0 n ^ n =0 n ^ 0 = n
    • 性质2:异或运算满足交换律和结合律(一批数,不管顺序,异或的结果都一样) 题目1:如何不用额外变量交换两个数(两个数不能指向同一个地址)
    a = a ^ b;
    b = a ^ b; //此时a = a ^ b    b = a ^ b ^ b = a ^ 0 = a
    a = a ^ b; // a = a ^ b ^ b = a ^ 0 = a
    

    题目2:一个数组中有一种数出现了奇数次,其他数都出现了偶数次,怎么找到并打印这种数

    int eor = 0;
    for (int i = 0; i < arr.length; i++){
      //跟数组中所有元素异或。出现偶数次的数,他们异或结果为0,(出现奇数次的数 -1)结果为0。最后就是0 ^ 出现奇数次的数 = 出现奇数次的数
    	eor = eor ^ arr[i];
    }
    System.out.println(eor); 
    

    题目3:怎么把一个int类型的数n,提取最右侧的1出来(例如:这个数的二机制是 00110000100 ,结果应该是100)

    n & ((~n) + 1)
    
    // ==============================
    n      	 = 00110000100
    ~n 	 	 = 11001111011
    ~n+1   	 = 11001111100
    n&((~n)+1) = 00000000100
    

    消除n最右侧的1 : n & (n - 1)

    题目4:一个数组中有两种数出现了奇数次,其他数都出现了偶数次,怎么找到并打印这两种数

2.算法链表结构栈队列递归行为哈希表

  • 反转单链表
/**
* Definition for singly-linked list.
* public class ListNode {
*     int val;
*     ListNode next;
*     ListNode(int x) { val = x; }
* }
*/
class Solution {
   public ListNode reverseList(ListNode head) {
       ListNode pre = null;//前一指针
       while(head != null){
           //1.记录next指针
           ListNode next = head.next;
           //2.改变指针指向
           head.next = pre;
           //3.head和pre都挪一位
           pre = head;
           head = next;
       }
       //注意:不是返回head。反转后head指向null,pre由指向null变为新的head。因此while中条件就是head!=null
       return pre;
   }
}
  • 删除链表中指定元素
Node removeValues(Node head,int num){
	while(head !=null){
    	if(head.value != num){
        	break;
        }
    }
    //head来到第一个不需要删除的位置,作为一个新的头部
    Node cur = head;
    Node pre = head;
    while(cur != null){
    	if(cur.value == num){
        	pre.next = cur.next;
        }else{
        	pre = cur;
        }
        cur = cur.next;
    }
    return head;
}
  • 实现一个特殊的栈,在基本功能的基础上,再实现返回栈中最小元素的功能 用两个栈,一个存数据,一个存当前最小值。输入数据:3 4 2 2 7 。左侧数据栈右侧最小值栈

  • 用栈结构实现队列,用队列结构实现栈
  • 哈希表HashMap
    • 使用哈希表增(put)、删(remove)、改(put)和查(get)的操作,可以认为时间复杂度为 O(1),但是常数时间比较大
    • 放入哈希表的东西,如果是基础类型,内部按值传递,内存占用是这个东西的大小。如果不是基础类型,内部按引用传递,内存占用是8字节
  • 有序表TreeMap
    • 有序表在使用时,时间复杂度都是O(logN)

3.算法归并排序与快速排序

  • 归并排序
    private void merge(int[] arr, int l, int h, int mid) {
        int[] help = new int[h - l + 1];
        //将数组arrCopy分为左右两部分[l ... mid]和[mid+1 ... h]进行归并
        int left = l; //左侧起点
        int right = mid + 1;//右侧起点

        // left遍历左边,right遍历右边。原则是哪个更小就把哪个拷贝到help数组里,然后left或者right加1。
        // 跳出循环的条件是:一侧遍历结束,也就是left或者right越界了。假如left越界,说明left遍历完了,将右侧剩下数据直接拷贝过来即可
        int i = 0;

        //left,right都没有越界
        while (left <= mid && right <= h) {
            help[i++] = arr[left] > arr[right] ? arr[right++] : arr[left++];
        }
        //left没有越界,right越界
        while (left <= mid) {
            help[i++] = arr[left++];
        }
        //left越界,right没有越界
        while (right <= h) {
            help[i++] = arr[right++];
        }

        //将归并结束,也就是l-->h的所有排好序数据。放到arr中
        for (i = 0; i < help.length; i++) {
            arr[i + l] = help[i];
        }
    }

    //递归的归并排序。调用跟斐波拉契数一样,是一个树形结构
    void mergeSort(int[] arr, int l, int h) {
        if (arr == null || arr.length < 2 || l == h) {
            return;
        }
        int mid = l + ((h - l) >> 1);//也可写成(l+h)/2,但有溢出风险

        mergeSort(arr, l, mid);
        mergeSort(arr, mid + 1, h);
        merge(arr, l, h, mid);
    }
    
    //====================================非递归的归并排序======================================
    void mergeSort2(int[] arr, int l, int h) {
        if (arr == null || arr.length < 2 || l == h) {
            return;
        }

        //1:将第0和1个数归并,第2和3个数归并......  mergeSize=2:将index为[0,1]]和[2,3]两个数组归并......  mergeSize=3:将index为[0,1,2]]和[3,4,5]两个数组归并
        int mergeSize = 1;//要归并的两个数组长度
        while (mergeSize < h) {
            int group = 2 * mergeSize;
            for (int i = 0; i < arr.length; i = i + group) {
                int left = i;
                int mid = left + mergeSize - 1;
                if (mid >= arr.length) {
                    //每次归并的两个数组长度为mergeSize。如果剩下数的个数小于mergeSize。那就已经有序了,不用管(都凑不齐两个数组)
                    break;
                }
                int right = Math.min(mid + mergeSize, arr.length - 1);
                //将[left,left+mergeSize]和[left+mergeSize+1,right]两个数组归并
                merge(arr, left, right, mid);
            }
            if (group > arr.length) {
                break;
            }
            mergeSize = mergeSize << 1;
        }
    }

//==========================非递归的归并排序,跟上面非递归排序效果一样=============================
    void mergeSort3(int[] arr, int l, int h) {
        int mergeSize = 1;
        while (mergeSize < arr.length) {
            int left = 0;
            while (left < arr.length) {
                int mid = left + mergeSize - 1;// 0 2
                if (mid >= arr.length) {
                    break;
                }
                int right = Math.min(mid + mergeSize, arr.length - 1);
                merge(arr, left, right, mid);
                left = right + 1;
            }
            if (mergeSize > arr.length / 2) {
                break;
            }
            mergeSize = mergeSize << 1;
        }
    }

归并排序的时间复杂度为nlogn,而选择,插入,冒泡排序都是n^2。以选择排序为例,每次循环只选择一位最小值,浪费了比较行为。而归并排序,假如要排序的数是[1 0 3 2],第一次循环结束变成[0 1 2 3 ],让部分有序了。所以归并排序会快点

求:一个数组中的某个数,右侧有多少个数比他大,或者多少个数比他小,可以用归并

  • 求一个数组的小和 在一个数组中,一个数左边比它小的数的总和,叫数的小和,所有数的小和累加起来,叫数组小和。求数组小和。 例子:

[1,3,4,2,5] 1左边比1小的数:没有

3左边比3小的数:1

4左边比4小的数:1、3

2左边比2小的数:1

5左边比5小的数:1、3、4、 2

所以数组的小和为1+1+3+1+1+3+4+2=16

解题思路:右侧比1大的数有4位,所以1会加4次。同理,3会加2次,4会加一次,2会加一次,所以小和为14 + 32 +4 +2 =16。对数组进行归并排序,每次merge的时候,只要左侧数比右侧数小,就加上左侧数

private int merge(int[] arr, int l, int h, int mid) {
        int[] help = new int[h - l + 1];
        //将数组arrCopy分为左右两部分[l ... mid]和[mid+1 ... h]进行归并
        int left = l; //左侧起点
        int right = mid + 1;//右侧起点

        // left遍历左边,right遍历右边。原则是哪个更小就把哪个拷贝到help数组里,然后left或者right加1。
        // 跳出循环的条件是:一侧遍历结束,也就是left或者right越界了。假如left越界,说明left遍历完了,将右侧剩下数据直接拷贝过来即可
        int i = 0;
        //小和
        int res = 0;
        //left,right都没有越界
        while (left <= mid && right <= h) {
            //h - right + 1  右侧当前位置到最后有多少个数比此时左侧数大
            res += arr[left] < arr[right] ? (h - right + 1) * arr[left] : 0;
            help[i++] = arr[left] >= arr[right] ? arr[right++] : arr[left++];
        }
        //left没有越界,right越界
        while (left <= mid) {
            help[i++] = arr[left++];
        }
        //left越界,right没有越界
        while (right <= h) {
            help[i++] = arr[right++];
        }

        //将归并结束,也就是l-->h的所有排好序数据。放到arr中
        for (i = 0; i < help.length; i++) {
            arr[i + l] = help[i];
        }
        return res;
    }


    int minAdd(int[] arr, int l, int h) {
        if (l == h) return 0;
        int mid = l + ((h - l) >> 1);
        return minAdd(arr, l, mid) + minAdd(arr, mid + 1, h) + merge(arr, l, h, mid);
    }

代码中跟归并排序不一样的地方就是merge方法中

  • 给定一个数组,左侧下标l和右侧下标h,给定数字num,要求将数组大于num的数排在左侧,大于num的数排在右侧,等于num的数排在中间,不要求有序
 int[] sort(int[] arr, int l, int h, int num) {
        if (l == h) {
            return new int[]{l, h};
        } else if (l > h) {
            return new int[]{-1, -1};
        }
        //小于区域和大于区域初始没有值
        int lowArea = l - 1;
        int highArea = h + 1;
        int index = l;
        while (index < highArea) {
            if (arr[index] < num) {
                //小于区域右扩
                swap(arr, index++, ++lowArea);
            } else if (arr[index] > num) {
                //大于区域左扩
                swap(arr, index, --highArea);
            } else {
                //相等时候循环正常走index++,不做处理
                index++;
            }
        }
        return new int[]{lowArea, highArea};
    }

  void swap(int[] arr, int i, int j) {
       int temp = arr[i];
       arr[i] = arr[j];
       arr[j] = temp;
  }
  //===================测试代码================
  val array = intArrayOf(3, 4, 1, 2, 4, 2, 5, 2)
  Test().sort(array, 0, array.size - 1, 4)
  for ((index, element) in array.withIndex()) {
          println("$index  -->  $element")
  }
  • 快速排序 基于上面的问题进而得出快速排序
 public static void swap(int[] arr, int i, int j) {
        int tmp = arr[i];
        arr[i] = arr[j];
        arr[j] = tmp;
    }

    //返回值:最后基准数的位置
    public static int partition(int[] arr, int L, int R) {
        if (L > R) {
            return -1;
        }
        if (L == R) {
            return L;
        }
        int lessEqual = L - 1;//小于等于区域初始值
        int index = L;
        while (index < R) {
            if (arr[index] <= arr[R]) {
                //小于等于基准值,小于区域右扩
                swap(arr, index, ++lessEqual);
            }
            index++;
        }
        //循环结束后划分为[小于等于区域,大于区域,基准值]
        //将小于区域下一位跟基准值交换。这样也就划分为[小于等于区域,基准值,大于区域]
        swap(arr, ++lessEqual, R);
        //返回基准值所在位置
        return lessEqual;
    }

    public static int[] netherlandsFlag(int[] arr, int L, int R) {
        if (L > R) {
            return new int[] { -1, -1 };
        }
        if (L == R) {
            return new int[] { L, R };
        }
        //以最右侧数为基准值,剩下的划分为三个区域,[小于基准值,等于基准值,大于基准值,基准值]
        //将基准值和大于区域第一个数交换
        int less = L - 1;
        int more = R;//不能R+1 ,否则为R的数据(基准值会被参与交换)
        int index = L;
        while (index < more) {
            if (arr[index] == arr[R]) {
                //小于区域右扩
                index++;
            } else if (arr[index] < arr[R]) {
                //大于区域左扩
                swap(arr, index++, ++less);
            } else {
                //相等时候循环正常走index++,不做处理
                swap(arr, index, --more);
            }
        }
        //将基准值和大于区域第一个数交换。这样也就划分为[小于基准值,等于基准值,大于基准值]
        swap(arr, more, R);
        return new int[] { less + 1, more };
    }

    //1.0版本
    public static void quickSort1(int[] arr) {
        if (arr == null || arr.length < 2) {
            return;
        }
        process1(arr, 0, arr.length - 1);
    }

    public static void process1(int[] arr, int L, int R) {
        if (L >= R) {
            return;
        }
        // 1.0版本,M是基准数的位置,小于基准数在L --> M-1  大于等于基准数在  M+1 --> R
        int M = partition(arr, L, R);
        process1(arr, L, M - 1);
        process1(arr, M + 1, R);
    }

    //2.0版本
    public static void quickSort2(int[] arr) {
        if (arr == null || arr.length < 2) {
            return;
        }
        process2(arr, 0, arr.length - 1);
    }

    public static void process2(int[] arr, int L, int R) {
        if (L >= R) {
            return;
        }
        //2.0版本,在1.0版本基础上(partition智能返回基准值所在位置),等于基准数的区域不再参与循环(netherlandsFlag可以返回)
        int[] equalArea = netherlandsFlag(arr, L, R);
        process1(arr, L, equalArea[0] - 1);
        process1(arr, equalArea[1] + 1, R);
    }

    //3.0版本
    public static void quickSort3(int[] arr) {
        if (arr == null || arr.length < 2) {
            return;
        }
        process3(arr, 0, arr.length - 1);
    }

    public static void process3(int[] arr, int L, int R) {
        if (L >= R) {
            return;
        }
        //3.0版本,在2.0版本基础上,随机选取一个数跟最右侧数交换
        swap(arr, L + (int) (Math.random() * (R - L + 1)), R);
        int[] equalArea = netherlandsFlag(arr, L, R);
        process1(arr, L, equalArea[0] - 1);
        process1(arr, equalArea[1] + 1, R);
    }

    // for test
    public static int[] generateRandomArray(int maxSize, int maxValue) {
        int[] arr = new int[(int) ((maxSize + 1) * Math.random())];
        for (int i = 0; i < arr.length; i++) {
            arr[i] = (int) ((maxValue + 1) * Math.random()) - (int) (maxValue * Math.random());
        }
        return arr;
    }

    // for test
    public static int[] copyArray(int[] arr) {
        if (arr == null) {
            return null;
        }
        int[] res = new int[arr.length];
        for (int i = 0; i < arr.length; i++) {
            res[i] = arr[i];
        }
        return res;
    }

    // for test
    public static boolean isEqual(int[] arr1, int[] arr2) {
        if ((arr1 == null && arr2 != null) || (arr1 != null && arr2 == null)) {
            return false;
        }
        if (arr1 == null && arr2 == null) {
            return true;
        }
        if (arr1.length != arr2.length) {
            return false;
        }
        for (int i = 0; i < arr1.length; i++) {
            if (arr1[i] != arr2[i]) {
                return false;
            }
        }
        return true;
    }

    // for test
    public static void main(String[] args) {
        int testTime = 500000;
        int maxSize = 100;
        int maxValue = 100;
        boolean succeed = true;
        for (int i = 0; i < testTime; i++) {
            //产生随机数
            int[] arr1 = generateRandomArray(maxSize, maxValue);
            int[] arr2 = copyArray(arr1);
            int[] arr3 = copyArray(arr1);
            //测试三个快排结果
            quickSort1(arr1);
            quickSort2(arr2);
            quickSort3(arr3);
            if (!isEqual(arr1, arr2) || !isEqual(arr2, arr3)) {
                succeed = false;
                break;
            }
        }
        System.out.println(succeed ? "Nice!" : "Oops!");

    }

因为最坏的情况时间复杂度为O(N^2) (每次sort结束,返回的都在最右侧)。所以随意选择一个数与最右侧数交换,这样N足够大的时候,时间复杂度为O(N*logN)

4.堆与比较器

堆结构就是用数组实现的完全二叉树结构
完全二叉树中如果每棵子树的最大值都在顶部就是大根堆
完全二叉树中如果每棵子树的最小值都在顶部就是小根堆
如果既不是大根堆,也不是小根堆,那就不是堆

  • 大根堆
public class BigHeap {
    private int[] arr;
    private int limit;
    private int heapSize;

    public BigHeap(int limit) {
        arr = new int[limit];
        this.limit = limit;
        heapSize = 0;
    }

    public boolean isEmpty() {
        return heapSize == 0;
    }

    public void push(int value) {
        if (heapSize >= limit) {
            throw new RuntimeException("超出限制");
        }
        int index = heapSize++;
        arr[index] = value;
        if (heapSize <= 1) {
            return;
        }
        //最后一个结点上浮
        while (arr[index] > arr[(index - 1) / 2]) {
            swap(index, (index - 1) / 2);
            index = (index - 1) / 2;
        }
    }

    public int pop() {
        if (heapSize == 0) {
            throw new RuntimeException("空堆");
        }
        if (heapSize == 1) {
            return arr[--heapSize];
        }
        int result = arr[0];
        swap(0, --heapSize);
        sink(0);
        return result;
    }

    //大根堆下沉
    private void sink(int index) {
        int left = (index * 2 + 1);
        while (left < heapSize) {
            //有右孩子,并且右孩子更大
            int maxChildIndex = (left + 1) < heapSize && arr[left] < arr[left + 1] ? (left + 1) : left;
            //当前节点和他的左右孩子,选出更大的
            maxChildIndex = (arr[index] > arr[maxChildIndex]) ? index : maxChildIndex;
            if (index == maxChildIndex) {
                //当前已经最大,结束循环
                break;
            }
            //与更大的孩子节点交换,等于父节点下沉到下一层
            swap(index, maxChildIndex);
            left = maxChildIndex * 2 + 1;
        }
    }

    private void swap(int i, int j) {
        int temp = arr[i];
        arr[i] = arr[j];
        arr[j] = temp;
    }
}
//==============测试代码======================
       val heap = BigHeap(100)
       heap.push(3)
       heap.push(7)
       heap.push(9)
       heap.push(2)
       heap.push(3)
       heap.push(8)

       while (!heap.isEmpty){
           println(heap.pop())
       }
  • 堆排序
public class HeapSort {
    public static void sort(int[] arr) {
        if (arr == null || arr.length < 2) {
            return;
        }
        //先建立大根堆

        //从上往下建立大根堆,每次新节点在最下面,然后上浮
        // O(N*logN)
        //for (int index = 0; index < arr.length; index++) {// O(N)
        //    swim(arr, index);// O(logN)
        //}

        //从下往上建大根堆,每次新节点在上面,然后下沉(相对于从上往下建立大根堆,此办法效率更高)
        for (int index = arr.length - 1; index >= 0; index--) {
            sink(arr, index, arr.length);
        }


        int heapSize = arr.length;
        //再依次将堆中第一个元素(最大的元素)移到数组最后面去。假如数组长度为5,index为0和index为4交换,调整0-3的数。
        // index为0和index为3交换,调整0-2的数...最后数组的数就从小到大排列了
        while (heapSize > 0) {
            swap(arr, 0, --heapSize);
            sink(arr, 0, heapSize);
        }
    }

    //节点上浮
    private static void swim(int[] arr, int index) {
        while (arr[index] > arr[(index - 1) / 2]) {
            swap(arr, index, (index - 1) / 2);
            index = (index - 1) / 2;
        }
    }

    //大根堆下沉
    private static void sink(int[] arr, int index, int heapSize) {
        int left = (index * 2 + 1);
        while (left < heapSize) {
            //有右孩子,并且右孩子更大
            int maxChildIndex = (left + 1) < heapSize && arr[left] < arr[left + 1] ? (left + 1) : left;
            //当前节点和他的左右孩子,选出更大的
            maxChildIndex = (arr[index] > arr[maxChildIndex]) ? index : maxChildIndex;
            if (index == maxChildIndex) {
                //当前已经最大,结束循环
                break;
            }
            //与更大的孩子节点交换,等于父节点下沉到下一层
            swap(arr, index, maxChildIndex);
            left = maxChildIndex * 2 + 1;
        }
    }
}

  • 比较器Comparator
//返回正数:obj2排在前面   返回负数:obj1排在前面
int compare(Object obj1, Object obj2)