• 算法很美(三)


    三、查找与排序(下)

    1、分治法(Divide and Conquer)

    分解(divide):将原问题分解成一系列子问题

    解决(conquer):递归地解决各子问题。若子问题足够小,则直接有解

    合并(combine):将子问题的结果合并成原问题的解

    优点:容易确定运行时间,提升一定效率(类比想象二分法)

    2、快速排序算法

    图解一下:

    img

    QuickSort(A,p,r)
     if p<r
        q = Partition(A,p,r)
        QuickSort(A,p,q-1)
        QuickSort(A,q+1,r)
    //A[q]为大小居中的数
    //找一个中间的数,其左边元素小于它,右边元素大于它;划分左边区域进行排序,右边区域进行排序;得出最终结果
    

    3、一遍单向扫描法

    图解一下:

    img

    QuickSort
      quickSort(A,p,r)
        if(p<r)
          q = partition(A,p,r)
          quickSort(A,p,q-1)
          qucikSort(A,q+1,r)
    
      partition(A,p,r)
        pivot = A[p]
        sp = p+1 //扫描指针
        bigger = r //右侧指针
        while(sp<=bigger):
          if(A[sp]<=pivot) //扫描元素小于主元
            sp++
          else
          	swap(A,sp,bigger) //扫描元素大于主元,二指针的元素交换,右指针左移
          	bigger--
    
        swap(A,p,bigger)
        return bigger 
    

    4、双向扫描法

    头指针往中间扫描,从左找到大于主元的元素,从右边找到小于等于主元的元素二者交换,继续扫描,直到左侧无大元素,右侧无小元素

    图解一下:

    img

      partiotion2(A,p,r) {
        pivot = A[p];
        left = p+1;
        right = r;
        while(left<=right) {
        //left不停往右走,直到遇到大于主元的元素
        while(left<=right&&A[left]<=pivot) left++; 
        //循环退出时,left一定指向第一个大于主元的位置
        while(left<=right&&A[right]>pivot) right--; 
        //循环退出时,right一定指向最后一个小于等于主元的位置
        if(left<right):
          swap(A,left,right);
        }
        //while退出时,两者交错,且right指向的是最后一个小于等于主元的位置,也就是主元应该在的位置
        swap(A,p,right);
        return right;
      }
    

    5、有相同元素值的快排——三分法

    图解了解:

    img

    6、快速排序的三种优化

    三点中值法:

    思路:将主元尽可能的放在中间,优化时间复杂度由O(n^2)--->O(nlgn)

    优化代码:

    //在p,r,min之间,选一个中间值作为主元
    int midIndex = p+((r-p)>>1); //中间下标
    int midValueIndex = -1;  //中值下标
    if(A[p]<=A[midIndex]&&A[p]>=A[r]){
        midValueIndex = p;
    }else if(A[r]<=A[midIndex]&&A[r]>=A[p]){
        midValueIndex = r;
    }else {
        midValueIndex = midIndex;
    }
    swap(A,p,midValueIndex);
    

    绝对中值法:

    思路:获取绝对的中值数,优化时间复杂度到O(n)

    优化代码:

    //获取绝对中间值
    getMedian(arr,p,r){
        size = r-p+1; //数组长度
        //每5个元素一组
        groupSize = (size%5==0)?(size/5):(size/5+1);
        //存储各小组中值
        medians[] = new [groupSize];
        indexOfMedian = 0;
        //对每一组进行插入排序
        for(j=0;j<groupSize;j++){
            //单独处理最后一组,因为可能不满5个元素
            if(j=groupSize-1){
                InsertSort(arr,p+j*5,r); //排序最后一组
                medians[indexOfMedian++] = arr[(p+j*5+r)/2]; //最后一组的中间那个
            }else {
                InsertSort(arr,p+j*5,p+j*5+4); //排序非最后一组的某个组
                medians[indexOfMedian++] = arr[p+j*5+2];  //当前组(排序后)的中间那个  
            }
        }
        //对medians排序
        InsertSort(medians,0,medians.length-1);
        return medians[medians.length/2];
    } 
    

    若排序列表较短,直接插入排序:

    具体原因:n(n-1)/2 < n(lgn+1) ,当n<8时,上式成立。

    7、归并排序(Merge Sort)

    -分解:将n个元素分成各含n/2个元素的子序列

    -解决:将两个子序列递归的排序

    -合并:合并两个已排序的子序列得到排序结果 【重点】

    思路:一分为二,开辟一个新的辅助空间,优化时间复杂度到O(n)
    

    图解一下:

    img

    实现代码:

    MergeSort
      mergeSort(A,p,r){
      	if (p<r) {
      		mid = p+((r-p)>>1);
      		mergeSort(A,p,mid);
      		mergeSort(A,mid+1,r);
      		merge(A,p,mid,r);
      	}
      }
      helper = [A.length];  //开辟的新空间
      merge(A,p,mid,r){
      	//先把A中的数据拷贝到helper中
      	copy(A,p,helper,p,r-p+1);
      	left = p; //左侧队伍的头指针,指向待比较元素
      	right = mid+1; //右侧队伍的头指针,指向待比较元素
      	current = p; //元素组的指针,指向待填入数据的位置
    
      	while(left<mid&&right<=r){
      		if(helper[left]<=helper[right]){
      			A[current] = helper[left];
      			current++;
      			left++;
      		}
      	}
      		while(left<=mid){
      			A[current] = helper[left];
      			current++;
      			left++;
      	}
      }
    

    8、最快效率求出乱序数组中第k小的数

    题目描述:以尽高的效率求出一个乱序数组中按数值顺序的第k个元素值。期望达O(n),最差O(n^2)

    图解一下:

    img

    代码实现:

    seclectK(A,p,r,k){
    	q = partition(A,p,r); //主元的下标
    	qK = q-p+1;  //主元是第几个元素
    	if(qK == k) returnA[q];
    	else if (qk>k) return seclectK(A,p,q-1,k);
    	else return seclectK(A,q+1,r,k-qK);
    }
    
    partition2(A,p,r);  //双向扫描
    

    9、趣味题:寻找发帖水王

    坊间风闻“水王”发帖数目超过帖子总数的一半,如果你有一个当前论坛上所有帖子的列表,其中帖子作者的ID也在表中,你能快速找出这个传说中的水王吗?

    思路:

    最原始的思路便是对所有的ID进行排序,统计ID出现的次数,若某个ID超过总数的一半则输出这个ID,时间复杂度为O(nlgn); 改进:如果每次删除2个不同的ID(无论是否包含水王的ID),那么剩下的ID中,水王的ID仍然是超过总数的一半,此时时间复杂度优化到O(n)
    

    原始代码实现:

    //缺点:要将整个数组进行重新排序
    Arrays.sort(arr);
    System.out.println(arr[arr.length/2]);
    

    改进代码实现:

    //侯选数,先定位第一个元素
    candidate = arr[0];
    //出现的次数
    nTimes = 1;
    //扫描数组
    for(i=1;i<arr.length;i++){
        //两两消减为0,把现在的元素作为候选值
        if(nTimes==0){
            candidate = arr[i];
            nTimes = 1;
            continue;
        }
        //遇到和候选值相同的,次数加一
        if(arr[i]=candidate){
            nTimes++;
        }else{
            nTimes--;
        }
        return candidate;
    }
    

    10、逆序对

    合并有序数组:给定两个排序后的数组A和B,其中A的末端有足够的缓冲空间容纳B,编写一个方法,将B合并入A并排序

    类似归并排序,图解一下:

    img

    11、用数组表示一棵二叉树

    二叉树的顺序存储结构就是使用一维数组存储二叉树中的结点,并且结点的存储位置,就是数组的下标索引。

    img

    子节点:2i+1,2i+2 父节点:(i-1)/2

    先序遍历(根左右):对访问到的每个结点*,先访问根结点,然后是左结点,然后是右结点

    A,B,D,G,H,I,C,E,J,F

    中序遍历(左根右) :对访问到的每个结点,先访问左结点,然后是根结点,然后是右结点

    G,D,I,H,B,A,C,E,F,J

    后序遍历(左右根):对访问到的每个结点,先访问左结点,然后是右结点,然后是根结点

    G,I,H,D,J,E,F,B,C,A

    public class TreeAndArray {
        //先序遍历
        private static void preOrder(int[] arr, int index){
            if (index>=arr.length){
                return;
            }
            System.out.println(arr[index]);//先输出根节点
            preOrder(arr, index*2+1);//输出左子树
            preOrder(arr,index*2+2);//输出右子树
        }
        //中序遍历
        private static void inOrder(int[] arr,int index){
            if (index>=arr.length){
                return;
            }
            inOrder(arr, index*2+1);
            System.out.println(arr[index]);
            inOrder(arr,index*2+2);
        }
    
        //后序遍历
        private static void postOder(int[] arr,int index){
            if (index>=arr.length){
                return;
            }
            postOder(arr, index*2+1);
            postOder(arr,index*2+2);
            System.out.println(arr[index]);
        }
    }
    

    12、堆排序

    二叉堆:近似完全二叉树 ,时间复杂度为O(nlogn) 并满足一下两个特性:

    1.父节点的键值总是大于或等于(小于或等于)任何一个子节点的键值

    2.每个结点的左子树和右子树都是一个二叉堆(都是最大堆或最小堆)

    任意节点的值都大于其子节点的值——大顶堆

    任意结点的值都小于其子节点的值——小顶堆

    img

    数组堆化:

    MinHeap(A){
    	n = A.length;
    	for i from n/2-1 down to 0{
    		MinHeapFixDown(A,i);
    	}
    }
    
    MinHeapFixDown(A,i,n){
    
    	//找到左孩子
    	left = 2*i+1;
    	right = 2*i+2;
    	//左孩子已经越界,i就是叶子结点
    	 (left>=n) {
    		return;
    	}
    	min = left;
    	if (right>=n) {
    		min = left;
    	}else{
    		if (A[right]<A[left]) {
    			min = right;
    		}
    	}
    	//min已经指向了左右孩子中较小的那个
    	
    	//如果A[i]比两个孩子都小,则不需要调整
    	if (A[i]<=A[min]) {
    		return;
    	}
    	//否则,找到两个孩子中较小的,与i换位置
    	temp = A[i];
    	A[i] = A[min];
    	A[min] = temp;
    	//小孩子那个位置的值发生了变化,i变更为小孩子那个位置,递归调整
    	MinHeapFixDown(A,min,n);
    }
    
    sort(A){
    	//先对A进行堆化
    	MinHeap(A);
    	for (x=n-1;x>=0 ;x-- ) {
    		//把堆顶,0号元素和最后一个元素对调
    		swap(A,0,x);
    		//缩小堆的范围,对堆顶元素进行向下调整
    		MinHeapFixDown(A,0,x-1)
    	}
    }
    

    13、计数排序

    图解了解:

    img

    实现代码:

    CountSort{                        //[] source
        [] helper = maxOf(source)+1;  //maxOf求出source数组最大值
        for(e : source) {
            helper[e]++;
        }
        current = 0;
        for(i=1; i<helper.length; i++){
            while(helper[i]>0) {
                source[current++] = i;
                helper[i]--;
            }
        }
    }
    

    分析:

    计数排序可以说是最快的排序算法,但是建议数据较为密集或范围较小的时候使用
    

    14、桶排序(Bucket Sort)

    图解事例:

    picture1.8

    有 11 个桶,编号从 0~10。每出现一个数,就将对应编号的桶中的放一个小旗子,最后只要数数每个桶中有几个小旗子就 OK 了。例如 2 号桶中有 1 个小旗子,表示 2 出现了一次;3 号桶中有 1 个小旗子,表示 3 出现了一次;5 号桶中有 2 个小旗子,表示 5 出现了两次;8 号桶中有 1 个小旗子,表示 8 出现了一次。

    时间复杂度在O(N)~O(NlgN)

    15、基数排序

    图解一下:

    img

    统一位数长度,接着从最低位开始,依次进行排序。时间复杂对O(KN) (K为最高位的次数 如:百-3) ,通常运用于10进制正数的排序。

    /**
     * 基数排序
     * 考虑负数的情况还可以参考: https://code.i-harness.com/zh-CN/q/e98fa9
     */
    public class RadixSort implements IArraySort {
    
        @Override
        public int[] sort(int[] sourceArray) throws Exception {
            // 对 arr 进行拷贝,不改变参数内容
            int[] arr = Arrays.copyOf(sourceArray, sourceArray.length);
    
            int maxDigit = getMaxDigit(arr);
            return radixSort(arr, maxDigit);
        }
    
        /**
         * 获取最高位数
         */
        private int getMaxDigit(int[] arr) {
            int maxValue = getMaxValue(arr);
            return getNumLenght(maxValue);
        }
    
        private int getMaxValue(int[] arr) {
            int maxValue = arr[0];
            for (int value : arr) {
                if (maxValue < value) {
                    maxValue = value;
                }
            }
            return maxValue;
        }
    
        protected int getNumLenght(long num) {
            if (num == 0) {
                return 1;
            }
            int lenght = 0;
            for (long temp = num; temp != 0; temp /= 10) {
                lenght++;
            }
            return lenght;
        }
    
        private int[] radixSort(int[] arr, int maxDigit) {
            int mod = 10;
            int dev = 1;
    
            for (int i = 0; i < maxDigit; i++, dev *= 10, mod *= 10) {
                // 考虑负数的情况,这里扩展一倍队列数,其中 [0-9]对应负数,[10-19]对应正数 (bucket + 10)
                int[][] counter = new int[mod * 2][0];
    
                for (int j = 0; j < arr.length; j++) {
                    int bucket = ((arr[j] % mod) / dev) + mod;
                    counter[bucket] = arrayAppend(counter[bucket], arr[j]);
                }
    
                int pos = 0;
                for (int[] bucket : counter) {
                    for (int value : bucket) {
                        arr[pos++] = value;
                    }
                }
            }
    
            return arr;
        }
    
        /**
         * 自动扩容,并保存数据
         *
         * @param arr
         * @param value
         */
        private int[] arrayAppend(int[] arr, int value) {
            arr = Arrays.copyOf(arr, arr.length + 1);
            arr[arr.length - 1] = value;
            return arr;
        }
    }
    

    16、十种排序算法总结

    img

    17、排序数组中找和的因子

    给定已排序的数组arr和k,不重复打印arr中所有相加和为k的不降序的二元组

    如: arr = {-8,-4,-3,0,2,4,5,8,9,10} k=10

    输出:(0,10)(2,8)

    思路:

    注意条件已排序的数组,所以我们采用二分查找中的双向扫描法,两个指针往中间扫,输出和为k的值。 这里判断不重复,使得left下标值不相同即可,因为k值是定的,left不同,right也不同了
    

    代码:

    private static void printTwoPair(int[] arr, int k) {
            //边界条件判断
    		if (arr == null || arr.length < 2) {
    			return;
    		}
        
    		int left = 0;
    		int right = arr.length - 1;
        	int sum = 0
    		while (left < right) {
    			sum = arr[left]+arr[right];
                if(sum<k){
                    ++left;
                }
                else if(sum>k){
                    --right;
                }
                else{
                    if (left == 0 || arr[left -1] != arr[left]){  //判断不重复
                        System.out.print(arr[left] +  "," +arr[right] + "
    ");
                    }
                    ++left;
                    --right;
                }
    	}
    }
    

    18、topK

    求海量数据(正整数)按逆序排列的前k个数(topK),因为数据量太大,不能全部存储在内存中,只能一个一个地从磁盘或者网络上读取数据,请设计一个高效的算法来解决这个问题。
    第一行:用户输入K,代表要求得topK

    随后的N(不限制)行,每一行是一个整数代表用户输入的数据

    用户输入-1代表输入终止

    请输出topK,从小到大,空格分割

    实际运用场景:比如从1千万搜索记录中找出最热门的10个关键词

    import java.util.Arrays;
    import java.util.Scanner;
    
    public class TopK {
        static int[] heap;
        static int index = 0;
        static int k;
    
        public static void main(String[] args) {
            Scanner sc = new Scanner(System.in);
            k = sc.nextInt();
            heap = new int[k];
            int x = sc.nextInt();
            while (x != -1) {
                deal(x);//处理x
                x = sc.nextInt();
            }
            printRs();
        }
    
        private static void printRs() {
            System.out.println(Arrays.toString(heap));
        }
    
        /**
         * 如果数据数量小于等于k,直接加入堆中
         * 等于k的时候,进行堆化
         * @param x
         */
        private static void deal(int x) {
            if (index < k) {
                heap[index++] = x;
                if (index == k) {
                    //堆化
                    makeMinHeap(heap);
                }
            } else
                //x和堆顶进行比较,如果x大于堆顶,x将堆顶挤掉并向下调整
                if (heap[0] < x) {
                    heap[0] = x;
                    MinHeapFixDown(heap, 0, k);
                    printRs();
                }
        }
    
        static void makeMinHeap(int[] A) {
            int n = A.length;
            for (int i = n / 2 - 1; i >= 0; i--) {
                MinHeapFixDown(A, i, n);
            }
        }
    
        static void MinHeapFixDown(int[] A, int i, int n) {
            // 找到左右孩子
            int left = 2 * i + 1;
            int right = 2 * i + 2;
            //左孩子已经越界,i就是叶子节点
            if (left >= n) {
                return;
            }
            int min = left;
            if (right >= n) {
                min = left;
            } else {
                if (A[right] < A[left]) {
                    min = right;
                }
            }
            //min指向了左右孩子中较小的那个
    
            // 如果A[i]比两个孩子都要小,不用调整
            if (A[i] <= A[min]) {
                return;
            }
            //否则,找到两个孩子中较小的,和i交换
            int temp = A[i];
            A[i] = A[min];
            A[min] = temp;
            //小孩子那个位置的值发生了变化,i变更为小孩子那个位置,递归调整
            MinHeapFixDown(A, min, n);
        }
    }
    
  • 相关阅读:
    java基础学习总结——GUI编程(二)
    003_Java笔记3:Eclipse添加jar包
    001_Eclipse编写第一个Java程序
    002_JavaSE笔记:单例模式
    001_IntelliJ IDEA详细安装步骤
    T4批量生成多文件
    T4模版引擎之基础入门
    T4模版引擎之生成数据库实体类
    localForage——轻松实现 Web 离线存储
    visual studio code插件精选
  • 原文地址:https://www.cnblogs.com/wangzheming35/p/12442148.html
Copyright © 2020-2023  润新知