算法刷题指北-基础篇-减治法和分治法

减治法

减治可以分为三种:减常量变量算法、减常因子算法、减开变规模算法;

减常因子主要就是将n的规模变为n-i的规模(i为常量,一般就是1)

  • 插入排序:(数组排序问题)将第一个视为有序,后面逐一插入有序序列的合适位置
  • 拓扑排序:(检查有向图有无环问题、确定拓扑顺序问题)对于给定的有向图,要求按照某一种顺序列出它的顶点,使得图的每一条边的起点的顶点总是在结束点之前;
  • 生成组合对象的算法:
    • 组合问题:对于一个没有重复元素的数组,其所有可能的序列
      • 直接插入:第一个元素,默认是有效序列,然后后面每次加一个元素(例如第二个序列就是 12、21)第三个就是(312、132、123、213、231、321)……直到生成新的序列直到第n个序列;缺点显然就是需要直到第n-1才可以直到第n个序列
      • Jackson:这个可以说专门实现这类问题:有点反人类;下面重点介绍
      • 字典法:字典法就是排序法,理论上有所序列就是这些元素所能组成的全部序列的排序(这个也可以用在查找某一序列重新排序,其小于原序列中最大的序列)
    • 子集

减常因子算法主要就是将n的规模变为n/i的规模(i为>1的常量、一般为2)

  • 折半查找
  • 假币问题(最简单的有一个假币)

减可变规模算法:主要是某些特定的问题,其结果和仅仅在某一区域的该问题的解的结果一样;

  • 插值查找(升级版折半查找。就是折半查找的开始的位置的改变):一般使用插值的方法找位置(先找弥合函数,通过弥合函数确定大概位置)
  • 计算中值和选择问题(例如求数组中第k小元素):这个比较常用,后面重点介绍

插入排序

//比较简单直接上代码,java代码类似,就是每次查找插入位置从右变开始查找嘛
//伪码,考试是伪代码。。。就省略java了
Function InsertSort(A[0……N-1])
    for(i:=1 to n-1) DO
        swap = A[i];
		for(j:=i-1;j>=0 AND A[j]>swap;j--)
            A[j+1] = A[j];
		A[j+1] = swap

拓扑排序

//简单来说就是遍历一次,将所有无输入端节点入栈,然后出栈的时候将该节点对应的输出变去掉,栈空就遍历一次图查找无输入端节点,直到图全部节点均完成进出栈

字典算法

遍历数组将数组组合成一个序列,然后:

  • 如果a(n) >a(n-1): 直接交换位置
  • 如果a(n)<a(n-1):先找出后面所有递减序列(即找最后的第一个递增的俩个节点)将这俩个节点的第一个节点和其后面所有节点中最小的但是比它大的节点交换,最后这个位置后的所有元素倒置,变成新序列(简单来说就是让这个位置后面变成递增),例如XXXX487532经过上面步骤就变为:XXXX523478

Jackson排序

同样先遍历一次数组组成序列:然后活动状态方向(即移动方向,但其不一定处于活动状态)规定为同一个方向,直到所有活动状态变为静止状态,规定如下:

  • 静止状态:
    • 其活动状态方向的元素比其大;(所以,最小元素就是静止状态)
    • 最大元素在活动状态方向的边界(即向左移动的状态但是其在第一个位置,向右移动则处于最后)
  • 活动状态:还能移动就是活动状态
  • 活动(移动):
    • 每次移动最大的活动状态的节点
    • 每次移动该节点后,比起大的节点的活动状态方向反转

折半查找

对于一个有序的数组,显然可以通过折半的方式快速查找目标位置;

很明显就是将数组分为三部分:a[begin……mid-1] 、a[mid] 、a[mid+1……end]按照这个思路修改begin(mid-1)、end(mid+1);mid就是(begin+end)/2即可

//很明显则版查找是一个递归算法,使用递归实现最简单
public int BSearch(int[] s,int search) {
	return doSearch(s,search,0,s.length);
}

public int doSearch(int[] s,int search,int begin,int end) {
    if(begin>end) {
        return -1;
    }
    int mid = (end+begin)/2;
    if(s[mid]==search) {
        return mid;
    } else if(s[mid]>search) {
        return doSearch(s,search,begin,mid-1)} else {
        return doSearch(s,search,mid+1,end);
    }
}

//当然也可以使用递推
public int BSearch(int[] s,int search) {
	for(int begin=0,end=s.length,int mid=(begin+end)/2;begin<=end;) {
        if(s[mid]==search) {
            return mid;
        } else if(s[mid]>search) {
            end = mid+1;
        } else {
            begin = emid-1;
        }
    return -1
}

计算中值和选择问题

例题:给定一个无序数组查找其中第k小的元素;(一般这么问肯定不想你使用排序算法)

快速分区算法实现

  • 选定一个元素(一般每次分区的第一个),遍历待处理分区,将比起大的放在一边,比其小的放在另一边,并且记录比其小的元素的个数,即可知道这个元素第几小;
  • 然后根据这个值再选择分区,再用方法1,直到这个元素就是要查找的第K小;

毫无疑问同样适合递归和递推

//递归    
public int partitionSearch(int []s, int k) {
        return s.length>=k?doPartitionbSearch(smkm0,s.length):-1;
}

public int doPartitionbSearch(int[] s,int k,int begin,int end) {
    int fb = begin;
    for(int i=begin+1;i<end;i++) {
        if(s[i]<s[begin]) {
			int swap = s[begin];
            s[begin++] = s[i];
            s[begin] = swap;
        }
    }
    if(begin+1==k) {
        return s[begin];
    } else if(begin+1<k){
        return doPartitionbSearch(s,k,begin+1,end);
    } else {
        return doPartitionbSearch(s,k,fb,begin-1);
    }
}

//递推
public int partitionSearchDiTui(int []s, int k) {
        if(s.length<k) {
            return -1;
        }
        for(int end = s.length,begin=0;;) {
            int fb = begin;
            for(int i=fb+1;i<end;i++) {
                if(s[i]<s[fb]) {
                    int swap = s[fb];
                    s[fb++] = s[i];
                    s[fb] = swap;
                }
            }
            if(fb+1<k) {
                begin++;
            } else if(fb+1==k){
                return s[fb];
            } else {
                end = begin;
            }
        }
    }

分治法

​ 分治法核心就是确定**:问题能划分为大致相等规模的子问题密切子问题的求解方式一样**;

最经典的分治法往往是可以递归的算法(例如二叉树的前序遍历),但是和减治法最大区别在于,分解后的子问题并不能使得问题范围变小,而是使其规模变小,常用的分支算法还有:

  • 合并排序首先将数组不断对半分、直到数组中只有一个元素;然后进入合并阶段,将数组俩俩合并,直到所有数组合并在一起; 这里的核心的就是合并算法,因为每个合并的数组都可以认为是一个有序的数组,按理来说每次只需要对俩个数组各遍历一次:一个是比较及赋值给原数组、一个是直接赋值给原数组(后面细说)
  • 二叉树算法遍历
    • 前序遍历:划分为左右子树递归,进入左子树前visit本节点;略
    • 中序遍历:划分为左右子树递归,进入右子树前visit本节点;略
    • 后序遍历:划分为左右子树递归,退出右子树后再visit本节点;略
    • 二叉树的构建:左右子树各得到除了n/2节点外的一半元素,直到自身持有的一般元素为空;
  • 快速排序首先选择一个节点(假设每次选首节点)作为哨兵划分数组,使得哨兵前的分区的元素都小于这个值后的分区的元素都大于这个值,不断重复直到所有分区都只有一个值或者为空;【这个算法尽管不需要更多内存,但是显然哨兵的选择对于分区的划分至关重要,最差情况就是n2
  • 大数乘法:纯数学,就是把大数分成俩部分,拆开成多项式、再拆开二次项变成一个复杂的多项式加法运算(略)

合并排序

合并排序的缺点就是:需要额外的存储空间保存过度数组;但是其时间复杂度很稳定,O(n)=nlog(n)-n+1;即nlog(n)

//递归
public int[] mergeSort(int[] s) {
    return s==null?null:doPartition(s);
}
public int[] doPartition(int[] s) {
    if(s.length>1) {
        int i=0;
        int s1[] = new int[s.length/2];
        for(;i<s1.length;i++) {
            s1[i] = s[i];
        }
       	int s2[] = new int[s.length-s1.length];
        for(int j=0;i<s.length;i++) {
            s2[j++] = s[i];
        }
        return doMerge(doPartition(s1),doPartition(s2),s);
    } else {
        return s;
    }
}
public int[] doMerge(int[] s1,int[] s2, int[] s) {
    int i=0,j=0,k=0;
    while(i<s1.length&&j<s2.length) {
        if(s1[i]>s2[j]) {
            s[k++] = s2[j++];
        } else {
            s[k++] = s1[i++];
        }
    }
    if(i==s1.length) {
        while(j<s2.length) {
            s[k++] = s2[j++];
        }
    } else {
        while(i<s1.length) {
            s[k++] = s1[i++];
        }
    }
    return s;
}



快速排序

快速排序的核心是:尽可能找到合适的使得哨兵左右俩个节点元素的分区大小一样;

    public int[] doQuickSort(int[] s,int begin,int end) {
        if (begin < end) {
            // 找寻基准数据的正确索引
            int index = partition(s, begin, end);
            //分别对于左右分区继续进行快排
            doQuickSort(s, begin, index-1);
            doQuickSort(s, index + 1, end);
        }
        return s;
    }

    public int partition(int[] s,int begin, int high) {
        //起始点
        int low = begin;
        //信标
        begin = s[begin];
        while (low<high) {
            //后面所有都大于信标
            while (low<high&&begin<=s[high]) {
                high--;
            }
            //信标处设置为high
            s[low] = s[high];
            //前面所有都小于信标
            while (low<high&&s[low]<=begin) {
                low++;
            }
            //否则交换
            s[high] = s[low];
        }
        //最后将信标放入
        s[low] = begin;
        //返回信标位置
        return low;
    }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

舔猫

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值