算法很重要也很难这个众所周知,不知是幸运还是不幸,现在的企业面试也开始卷算法了。我自己是没什么算法底子,老实说现在让我手写个简单排序我都写不好的,就从这里开始吧。
先放代码地址,拷了好多别人的资料,老实说看的一脸懵,会慢慢删掉别人的代码,自己再抄一遍的
一、等差数列和等比数列
本人比较懒,觉得学算法最重要的就是“领会精神”,搞懂了算法的思想,代码也就是熟能生巧的事,反正会忘谁在乎呢。那么就从大家都知道的,高中数学里两个两个有意思的推理说起吧。
等差数列求和这个嘛,有一个很有名的从1加到100的故事[(1+100)*100/2],公式大概都能脱口而出了(首项+尾项)*项数/2,表达式为(n^2+n)/2
重点说说等比数列等比数列求和,额...还是上链接吧你应该想想,等比数列的求和是怎么来的? - 知乎
简诉推导过程如下:a是首项、q是公比
Sn= a + a1*q + a*q^2 + ... + a*q^n-1;
Sn*q= a1*q + a*q^2 + ... + a*q^n-1 + a*q^n
相减得:
Sn*q - Sn = a*q^n - a
Sn*(q-1) = a*(q^n - 1)
Sn = a*(q^n - 1)/(q-1)
其实就是巧妙的使用了错位相减的方法,算法其实同数学思想一样只不过是换了个名字而已,都是找规律,通过正向的逆向的规律来完成数据的批量处理。
二、从两数之和到abandon
话说某一天兴冲冲的打开leetcode准备刷算法,然后迎面而来的就是两数之和,两个月后又兴冲冲的打开leetcode,没错还是两数之和^_^,就想起了我大学时兴冲冲的说我要考研,背了俩月英语单词,翻来覆去的abandon,最后真就abandon了……
能放到简单难度的第一题,还是有他独特的魅力的。首先这个题目足够简单,求和嘛,两层循环挨个加加到刚好等于目标值为止,别说这方法low总比写不出来好。
某人就这么写完开始看评论区,然后就被卷佬们惊到了,好得是求和,甭管怎么算不是加法就是减法吧,都不是!大佬们连加减乘除都是从位运算写起,我算知道我算法怎么老刷老第一题了,太特么劝退了。
就不废话了,这题我从评论区抄了两个解法,
第一个:工程法,就是类似问题在我们日常工作中通常会使用的方法,就是一次运算[初始化]把所有可能性缓存起来[map/redis],等用的时候直接从缓存取,第一次O(n^2),后面O(1)
第二个:比赛法,就是要速度更快内存更小,时间复杂和空间复杂都尽可能好的方法。还是逆向思维,比如1+2=3,3是target,当前如果是1,其实我们就是在找2,2就是所谓的补数,只要每次把补数及其下标存起来,在遍历到队末之前,我们必然能找到“一对”符合target的数字。时间复杂度和空间复杂度都要比第一个方法小一个量级,了解这种写法才是我们学算法的目的——领会她的思想。
int[] no = {3,2,4};
int target = 6;
@Test
public void twoSum1(){
//工程缓存法
Map map = new HashMap();
for (int i = 0; i < no.length-1; i++) {
for (int j = i+1; j < no.length; j++) {
if(i != j){
map.put(no[i]+no[j], new int[]{i,j});
}
}
}
int[] index = (int[])map.get(target);
System.out.println(Arrays.toString(index));
}
@Test
public void twoSum2(){
//补数 逆向思维
Map<Integer, Integer> map = new HashMap();
int[] index = new int[2];
for (int i = 0; i < no.length; i++) {
if(map.containsKey(no[i])){
index[0] = map.get(no[i]);//map k补数,匹配的数; v该数的下标
index[1] = i;
break;
}
map.put(target - no[i], i);
}
System.out.println(Arrays.toString(index));
}
三、数组排序
数组排序主要讲了菜鸡三兄弟[冒泡、选择、插入]和NB三兄弟[快排、堆排、归并排序],代码上面的git都有的,为了凑篇幅,这边就单独贴出来,随便搭个java环境就可以运行这段代码。
package com.cheng.algorithm;
import org.junit.Before;
import org.junit.Test;
import java.security.SecureRandom;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Consumer;
public class 排序 {
private static final int cycleNo = 10;
private static final int maxArrayLength = 1500;
private static final int maxNo = 100;
@Before
public void init (){
}
/**
* 排序结果测试
* 总结:三大nlogn排序
* 堆排:理解复杂,速度相对较慢[无意义的交换较多],在工程实践中,其自动排序的思想很有用
* 快排:理解中等,速度相对最快[受数据情况影响,最坏情况时最慢],在拆的过程中完成排序
* 归并排序:理解较简单,速度稳定[占用内存],在合的过程中完成排序
*/
@Test
public void test(){
for (int i = 0; i < cycleNo; i++) {
int[] arr = getRandomArray();
int[] arr1 = copyArray(arr);
int[] arr2 = copyArray(arr);
int[] arr3 = copyArray(arr);
int[] arr4 = copyArray(arr);
int[] arr5 = copyArray(arr);
int[] arr6 = copyArray(arr);
testSort(t->bubble(arr), arr);
testSort(t->selection(arr1), arr1);
testSort(t->insert(arr2), arr2);
testSort(t->heapsort(arr3), arr3);
testSort(t->quickSort(arr4), arr4);
testSort(t->quickSort1(arr5), arr5);
testSort(t->mergeSort(arr6), arr6);
System.out.println("=============================");
}
}
/**
* 调试代码用
*/
@Test
public void easyTest(){
int[] no = {9,8,7,6,5,4,3,2,1};
int[] no1 = {1,3,9,8,7,6};
// bubble(no);
// bubble1(no);
// selection(no);
// insert(no);
// heapsort(no);
// quickSort(no1);
// quickSort1(no1);
// netherlandsFlag(no1, 0, no1.length-1, 6);
mergeSort(no);
print(no);
}
/**
* 冒泡排序
* 相邻交换,一次外循环排好一个数
*/
public void bubble(int[] no){
int k = 0;
for (int i = 0; i < no.length-1; i++) {//此处是总体遍历次数,10个元素最多遍历9次可完成排序
for (int j = 0; j < no.length-i-1; j++) {//此处i代表已经交换完成不需要再次比较交换的元素
if(no[j] > no[j+1]){
swap(no, j, j+1, k++);
}
}
}
System.out.println("冒泡排序: 数组长度为:"+no.length+";交换次数为:"+k);
// print(no);
}
public void bubble1(int[] no){
int k = 0;
for(int i = no.length -1; i > 0; i--){
for(int j =0; j< i; j++){
if(no[j] > no[j+1]){
swap(no, j, j+1, k++);
}
}
}
// System.out.println("循环次数等差数列求和"+(1+no.length-1)*(no.length-1)/2);
System.out.println("冒泡排序: 数组长度为:"+no.length+";交换次数为:"+k);
// print(no);
}
/**
* 选择排序
* 每次选择一个最小的放在队首
*/
public void selection(int[] no){
int k = 0;
for (int i = 0; i < no.length-1; i++) {//循环数组长度次数-1[最后一个肯定是最大,不需要再比较],每次获取列表中最小的元素放在数组队首
int min = no[i];
for(int j = i; j< no.length; j++){//通过比较交换,获取列表中最小元素
if(min > no[j]){
k++;
int temp = no[j];
no[j] = min;
min = temp;
}
}
no[i] = min;
}
System.out.println("选择排序: 数组长度为:"+no.length+";交换次数为:"+k);
// print(no);
}
/**
* 插入排序
* 像插牌一样从后往前保证有序, 排序效率和数据状况有关系
*/
public void insert(int[] no){
int k = 0;
for (int i = 1; i < no.length; i++) {//插入排序元素的起始位置,从第2个开始往前排
for(int j=i; j>0; j--){//两两交换,保证局部有序
if(no[j-1] > no[j]){
swap(no, j, j-1, k++);
}
}
}
System.out.println("插入排序: 数组长度为:"+no.length+";交换次数为:"+k);
// print(no);
}
/**
* 堆排序
* 1.构建大根堆[利用堆的自动排序功能logn]; 2.将堆顶元素和队末元素交换[最大],输出最后一个元素
* 数组表示堆:节点i,父节点(i-1)/2,左孩子2*i+1,右孩子2*i+2
* 比如: 下标为0,F:0 L:1 R:2; 下标为3 F:1 L:7 R:8
*/
public void heapsort(int[] no){
Map<String, Integer> kmap = new HashMap<>();//计数器
kmap.put("k",0);
int k = 0;
for (int i = 0; i < no.length; i++) {
heapInsert(no, i, kmap);
}
int size = no.length-1;
swap(no,0, size, k++);//获取第一个最大值[根节点放到最后]
while(size > 0){
heapify(no, 0, size, kmap);
swap(no, 0, --size, k++);//将根与队末交换并"弹出"
}
kmap.put("k", kmap.get("k")+k);
System.out.println("堆排序: 数组长度为:"+no.length+";交换次数为:"+kmap.get("k"));
// print(no);
}
public void heapInsert(int[] no, int i, Map<String, Integer> kmap){
int k = 0;
while(no[i] > no[(i-1)/2]){ //构建大根堆:从下往上调整。和父节点比大小,比父节点大则交换
swap(no, i, (i-1)/2, k++);
i = (i-1)/2; //更新当前节点为父节点
}
kmap.put("k", kmap.get("k")+k);
}
public void heapify(int[] no, int i, int end, Map<String, Integer> kmap){//从上[前]往下[后]调整
int left = 2*i+1; //左孩子节点
int k = 0;
while(left < end){
int largest = left +1 < end && no[left+1] > no[left] ? left+1 : left; //选择左右孩子节点值较大的一个
largest = no[largest] > no[i] ? largest : i;//返回孩子节点和当前节点较大的一个
if(largest == i){//当前节点大于孩子节点,堆调整完成
break;
}
swap(no, largest, i, k++);
i = largest;//更新当前节点为孩子节点
left = 2*i+1;
}
kmap.put("k", kmap.get("k")+k);
}
/**
* 荷兰国旗问题
* 给定一个数组arr和一个数num,请把小于num的数放在左边,等于num的数放在中间,大于num的数放在右边。要求额外空间为O(1),时间为O(n)
* 思路:双指针法,夹逼[同样、其中任一指针完全遍历也可以完成该功能,双指针加速]
* 划定小于区域和大于区域,
* 当发现遍历的值小于num时,将遍历的值移动到小于区域[将当前值和小于区域的下一个值交换]
* 当发现遍历的值大于num时,将遍历的值移动到大于区域[将当前值和大于区域的前一个值交换]
*
*/
public void netherlandsFlag(int[] no, int l, int r, int num){
int less = l-1; //less小于区域的右边界
int more = r+1; //more大于区域的左边界
int k = 0;
while(l < more){//l当前位置 当前位置不碰上右边界
if(no[l]< num){
swap(no, ++less, l++, k++);//当前区域和小于区域的下一个数交换,当前位置和小于区域边界右移一个位置
}else if(no[l] > num){
swap(no, l, --more, k++);
}else{
l++;
}
}
print(no);
}
/**
* 快速排序 - 荷兰国旗法
* 设置基准值[队首、队尾、队中]
* 采用双指针法,从数组两边分别比对,把小于基准值的放在左边,大于基准值的放在右边
* 利用递归算法和分治的思想继续完成排序
*/
public void quickSort(int[] no){
Map<String, Integer> kmap = new HashMap<>();//计数器
kmap.put("k",0);
quickSort(no, 0, no.length-1, kmap);
System.out.println("快速排序: 数组长度为:"+no.length+";交换次数为:"+kmap.get("k"));
// print(no);
}
public void quickSort(int[] no, int l, int r, Map<String, Integer> kmap){
int k = 0;
if(l < r){
swap(no, l+(int)(Math.random()*(r-l+1)), r, k++); //随机基准数
kmap.put("k", kmap.get("k")+k);
int[] p = partion(no, l, r, kmap);
quickSort(no, l, p[0]-1, kmap);//小于区域
quickSort(no, p[1]+1, r, kmap);//大于区域
}
}
public int[] partion(int[] no, int l, int r, Map<String, Integer> kmap){
int less = l-1;
int more = r;//r作为基准值,调整完后换到中间,因此初始r即为右边界不需要r+1
int k = 0;
while(l < more){
if(no[l] < no[r]){ //经典快排,当前值和最后一个值[基准值]比较
swap(no, ++less, l++, k++);
}else if(no[l] > no[r]){
swap(no, --more, l, k++);
}else{
l++;
}
}
swap(no, more, r, k++);//将基准值和大于区域左边界的值交换
kmap.put("k", kmap.get("k")+k);
return new int[]{less+1, more};//返回等于区域的左边界和右边界,小于区域+1为左边界,more因为刚换过,因此就是等于区域的右边界,可以使得等于区域值不参与后面的排序
}
/**
* 双指针交换法,从数组两端开始遍历,交换小于基准值和大于基准值的数
*/
public void quickSort1(int[] no){
Map<String, Integer> kmap = new HashMap<>();//计数器
kmap.put("k",0);
quickSort1(no, 0, no.length-1, kmap);
System.out.println("快速排序1: 数组长度为:"+no.length+";交换次数为:"+kmap.get("k"));
// print(no);
}
public void quickSort1(int[] no, int l, int r, Map<String, Integer> kmap){
if(l > r){
return;
}
int k = 0;
swap(no, l, l+(int)(Math.random()*(r-l+1)), k++); //随机基准数 队头
int base = no[l]; //基准值
int less = l;//左指针
int more = r;//右指针
while(less != more ){//while 条件不满足则停止
while(less < more && no[more] >= base){//从右向左, 找到第一个小于基准值的数, 两个while顺序不能调换
more--;
}
while(less < more && no[less] <= base){//从左向右, 找到第一个大于基准值的数, less放下面则less作为临界值
less++;
}
if(less < more){//相等时不交换
swap(no, less, more, k++);
}
}
swap(no, less, l, k++); //把小于区域边界的值和基准值交换,保证基准值在中间
kmap.put("k", kmap.get("k")+k);
quickSort1(no, l, less-1, kmap);
quickSort1(no, less+1, r, kmap);
}
/**
* 归并排序
* 依然是利用递归和分治的思想,通过递归的方式将数组拆散,在合并的过程中使用辅助数组完成排序
* 不同于其他几个排序方式,不需要交换,但需要使用O(n)的额外空间
* 类似问题:小和
*/
public void mergeSort(int[] no){
if(no == null || no.length < 2){
return;
}
mergeSort(no, 0, no.length-1);
System.out.println("归并排序: 数组长度为:"+no.length+";交换次数为:0");
}
public void mergeSort(int[] no, int l, int r){
if(l == r){
return;
}
int mid = l + ((r-l) >> 1); //从中间将数组拆成左半边和右半边
mergeSort(no, l, mid);
mergeSort(no, mid+1, r);
merge(no, l, mid, r);//从左到右,按拆分的次序合并
}
public void merge(int[] no, int l, int m, int r){
int[] help = new int[r-l+1]; //辅助数组
int i = 0; //辅助数组填值下标
int p1 = l; //左半边指针
int p2 = m+1; //右半边指针
while(p1 <= m && p2 <= r){
//左边小填左边左边指针移动,右边小填右边右边指针移动
help[i++] = no[p1] < no[p2] ? no[p1++] : no[p2++];
}
//两个while只会执行一个,将左边或右边剩余的值顺序填到辅助数组里
while(p1 <= m){
help[i++] = no[p1++];
}
while(p2 <= r){
help[i++] = no[p2++];
}
//将辅助数组的值回填到原数组
for (i = 0; i < help.length; i++) {
no[l+i] = help[i];
}
}
/**
* 交换器
*/
public static void swap1(int [] no, int i, int j, int k){
//引入临时元素 空间换时间
int temp = no[j];
no[j] = no[i];
no[i] = temp;
}
public static void swap2(int[] no, int i, int j, int k){
if(i != j){
no[i] = no[i] + no[j];
no[j] = no[i] - no[j];
no[i] = no[i] - no[j];
}
}
public static void swap(int[] no, int i, int j, int k){
//两数相同异或为0
if(i != j){
no[i] = no[i] ^ no[j];
no[j] = no[i] ^ no[j];//no[i] ^ no[j] ^ no[j] 此处no[j],抵消变为no[i]
no[i] = no[i] ^ no[j];//no[i] ^ no[j] ^ no[i] 此处的no[j]相当于是原始的no[i],抵消变为no[j]
}
}
/**
* 水平打印
*/
public static void print(int [] no){
System.out.println(Arrays.toString(no));
}
/**
* int数组对数器
* @param sort 排序方法
* @param arr 排序数组
*/
public static void testSort(Consumer<int[]> sort, int[] arr){
int[] arr2 = copyArray(arr);
sort.accept(arr);
Arrays.sort(arr2);
if(!isEqual(arr, arr2)){
System.out.println("-------------------------------------------");
System.out.println("排序异常,数组为:");
print(arr);
print(arr2);
System.out.println("-------------------------------------------");
}
}
private static int[] getRandomArray(){
int[] arr = new int[getRandomNo(maxArrayLength)];
for (int i = 0; i < arr.length; i++) {
arr[i] = getRandomNo(maxNo) - getRandomNo(maxNo);//支持负数
}
return arr;
}
private static int getRandomNo(int no){
SecureRandom random = new SecureRandom();
int randomNo = random.nextInt(no + 1);
return randomNo == 0?getRandomNo(no):randomNo;
}
private 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;
}
private 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;
}
}