手撕算法——快速排序及相關題型

快速排序及它的變形題型在面試中經常被問到,本文進行一下總結。

快速排序

實現快速排序的關鍵便是partition函數,這個函數的主要功能是把比選擇數字小的數字移到數組的左邊,把比選擇數字大的數字移到數組的右邊。
主要實現分爲以下四步:
第一步:找基準數,並把它放到數組的末尾
第二步:使用small標記劃分區間,初始值爲-1,意味着劃分區間無值
第三步:遍歷數組,將比基準值小的值放入劃分區間
第四步:將選擇的數字也放入劃分區間
代碼如下

public static int partition(int[] arr,int len,int start,int end){
        if(arr==null||len<=0||start<0||end>=len){
            return -1;
        }
        //找基準數,並將基準數放到數組的末尾
        int index=start+(int)(Math.random()*(end-start+1));
        swap(arr,index,end);

        //劃分區間
        int small=start-1;
        for(index=start;index<end;index++){
            if(arr[index]<arr[end]){
                small++;
                if(index!=small){
                    swap(arr,index,small);
                }
            }
        }
       
        //將選擇的數字也放入到區間
        small++;
        swap(arr,small,end);
        return  small;
    }
    public static void swap(int[] arr,int index1,int index2){
        int tmp=arr[index1];
        arr[index1]=arr[index2];
        arr[index2]=tmp;
    }

快排的實現

public void quicksort(int[] data,int start,int end){
	if(start==end){
		return;
	}
	int index=partition(arr,start,end);
	if(index>start){
		quicksort(arr,start,index-1);
	}
	if(index<end){
		quicksort(arr,index+1,end);
	}
	
}

時間複雜度分析,因爲快排也採用分而治之的思想,因此在最優情況下(partition每次劃分的很均勻)時間複雜度爲O(nlog2n),最差的情況下時間複雜度爲n^2;

相關題型

數組中出現次數超過一半的數字

面試題39. 數組中出現次數超過一半的數字
思路一:
將數組排序,然後數組中位數,則爲要求的結果,數組排序的時間複雜度爲O(nlogn);

思路二:
使用partition

class Solution {
    public int majorityElement(int[] nums) {
        /*
        直接找nums.length/2大的數
        */
        if(nums==null){
            return -1;
        }
        int len=nums.length;
        int middle=len>>1;
        int start=0;
        int end=len-1;
        
        int index=partition(nums,len,start,end);
        while(index!=middle){
            if(index>middle){
                end=index-1;
                index=partition(nums,len,start,end);
            }else{
                start=index+1;
                index=partition(nums,len,start,end);
            }
        }

        int res=nums[middle];
        if(!cheackMoreThanHalf(nums,len,res)){
            res=0;
        }

        return res;
    }
    public int partition(int[] nums,int len,int start,int end){
        if(nums==null||len<=0||start<0||end>=len||start>end){
            return -1;
        }

        int index=start+(int)(Math.random()*(end-start+1));
        swap(nums,index,end);
        int small=start-1;
        for(index=start;index<end;index++){
            if(nums[index]<nums[end]){
                small++;
                if(index!=small){
                    swap(nums,index,small);
                }
            }
        }
        small++;
        swap(nums,small,end);
        return small;
    }
    public void swap(int[] nums,int index1,int index2){
        int tmp=nums[index1];
        nums[index1]=nums[index2];
        nums[index2]=tmp;
    }
    
    public boolean cheackMoreThanHalf(int[] nums,int len,int number){
        int times = 0;
        for (int i = 0; i <len; i++) {
            if (nums[i] == number)
                times++;
        }
        boolean isMoreThanHalf = true;
        if (times * 2 <= len) {
             isMoreThanHalf = false;
        }
        return isMoreThanHalf;
    }
}

思路三:
摩爾投票法

class Solution {
    public int majorityElement(int[] nums) {
        int x=0,votes=0;
        for(int num:nums){
            if(votes==0) x=num;
            votes+=num==x?1:-1;
        }
        return x;
    }
}

思路四:使用map記錄出現的次數,不再贅述

最小的k個數

class Solution {
    public int[] getLeastNumbers(int[] arr, int k) {
        if(arr==null||arr.length<=0||k<=0){
            return new int[0];
        }

        int res[]=new int[k];
        int len=arr.length;

        int start=0;
        int end=len-1;
        int index=partition(arr,start,end);//最小的k個數
        while(index!=k-1){
            if(index>k-1){//比k-1多,在前index中找
                end=index-1;
                index=partition(arr,start,end);
            }else{//比k-1少,從start開始找
                start=index+1;
                index=partition(arr,start,end);
            }
        }

        for(int i=0;i<k;i++){
            res[i]=arr[i];
        }

        return res; 
    }

    int partition(int[] arr,int start,int end){
        if(arr==null||arr.length<=0||start<0||end>=arr.length){
            return -1;
        }
        int index=start+(int)(Math.random()*(end-start+1));
        swap(arr,index,end);

        int small=start-1;
        for(index=start;index<end;index++){
            if(arr[index]<arr[end]){
                small++;
                if(small!=index){
                    swap(arr,index,small);
                }
            }
        }

        small++;
        swap(arr,small,end);
        return small;
    }

    public void swap(int[] arr,int i,int j){
        int tmp=arr[i];
        arr[i]=arr[j];
        arr[j]=tmp;
    }
}

查找第k大的數字

215. 數組中的第K個最大元素
題目描述:
在未排序的數組中找到第 k 個最大的元素。請注意,你需要找的是數組排序後的第 k 個最大的元素,而不是第 k 個不同的元素。

class Solution {
    public int findKthLargest(int[] nums, int k) {
        if(nums==null||nums.length<=0||k<=0||k>nums.length){
            return 0;
        }
        int start=0;
        int len=nums.length;
        int end=nums.length-1;
        int index=partition(nums,start,end);
        int target=len-k;
        while(index!=target){
             if(index<target){
                 index=partition(nums,index+1,end);  
             }else{
                index=partition(nums,start,index-1);
             }   
        }
        return nums[index];
    }

      int partition(int[] arr,int start,int end){
        if(arr==null||arr.length<=0||start<0||end>=arr.length){
            return -1;
        }
        int index=start+(int)(Math.random()*(end-start+1));
        swap(arr,index,end);

        int small=start-1;
        for(index=start;index<end;index++){
            if(arr[index]<arr[end]){
                small++;
                if(small!=index){
                    swap(arr,index,small);
                }
            }
        }

        small++;
        swap(arr,small,end);
        return small;
    }
    public void swap(int[] arr,int i,int j){
        int tmp=arr[i];
        arr[i]=arr[j];
        arr[j]=tmp;
    }


}

注意這道題的另一個思路是使用堆來解決。使用一個大小爲k的小頂堆,堆頂維護這最小的元素,當新的元素比堆頂元素還要小,那麼不用管它,否則刪除堆頂元素,把較大的元素加入,這樣堆頂就一直維護着第k個大的元素。
使用java中的優先隊列PriorityQueue實現如下:

 public int findKthLargest(int[] nums, int k) {
        if(nums==null||nums.length<=0||k<=0){
            return 0;
        }
        PriorityQueue<Integer> priorityQueue=new PriorityQueue<>(k,(a,b)->a-b);
        for(int num:nums){
            int size=priorityQueue.size();
            if(size<k){
                priorityQueue.add(num);
            }else{
                if(num>priorityQueue.peek()){
                    priorityQueue.poll();
                    priorityQueue.add(num);
                }
            }
        }
        return priorityQueue.peek();
    }
}

時間複雜度分析
向大小爲k的堆中添加元素的時間複雜度爲O(logk),我們將重複該操作N次,故總時間負責度爲O(NlogN).

最大的k個數

題目鏈接:打印N個數組整體最大的Top K
有N個長度不一的數組,所有的數組都是有序的,請從大到小打印這N個數組整體最大的前K個數。
例如,輸入含有N行元素的二維數組可以代表N個一維數組。
219, 405, 538, 845, 971
148, 558
52, 99, 348, 691
再輸入整數k=5,則打印:
Top 5: 971, 845, 691, 558, 538
[要求]
時間複雜度爲O(k \log k)O(klogk),空間複雜度爲O(k \log k)O(klogk)

public class NiukePrinttopN {
    public static void main(String[] args) {
        Scanner scanner=new Scanner(System.in);
        int row=scanner.nextInt();
        int k=scanner.nextInt();
        PriorityQueue<Integer> priorityQueue=new PriorityQueue<>(k,(a, b)->a-b);
        for(int i=0;i<row;i++){
            int number=scanner.nextInt();
            for(int j=0;j<number;j++){
                int num=scanner.nextInt();
                int size=priorityQueue.size();
                if(size<k){
                    priorityQueue.add(num);
                }else if(num>priorityQueue.peek()){
                    priorityQueue.poll();
                    priorityQueue.add(num);
                }
            }
        }
        int res[]=new int[k];
       for(int i=0;i<k;i++){
            res[i]=priorityQueue.poll();
       }
       for(int i=k-1;i>0;i--){
           System.out.print(res[i]+" ");
       }
        System.out.print(res[0]);
    }
}

轉化爲幾乎有序序列的最小代價

題目描述:
.給一個長度爲偶數n的序列中,前n/2個元素裏面的最大值小於等於後n/2個元素裏的最小值,稱爲“幾乎有序”,可以對序列交換任意多次,兩個不同的需要i,j交換每次的代價是i-j的絕對值。求將給定序列,轉化爲幾乎有序序列的最小代價。

public class Main {
    public static void main(String[] args) {
        int[] arr={1,5,6,9,2,4};
        int len=arr.length;
        int start=0;
        int end=len-1;
        int index=partition(arr,len,start,end);
        int target=len>>1;
        System.out.println(target);
        while(index!=target){
            if(index<target){
               index= partition(arr,len,index+1,end);
            }else{
               index =partition(arr,len,start,index-1);
            }
        }
        for(int e:arr){
            System.out.print(e+" ");
        }
    }
    public static int partition(int[] arr,int len,int start,int end){
        if(arr==null||len<=0||start<0||end>=len){
            return -1;
        }
        //找基準數,並將基準數放到數組的末尾
        int index=start+(int)(Math.random()*(end-start+1));
        swap(arr,index,end);

        //劃分區間
        int small=start-1;
        for(index=start;index<end;index++){
            if(arr[index]<arr[end]){
                small++;
                if(index!=small){
                    swap(arr,index,small);
                }
            }
        }

        //將選擇的數字也放入到區間
        small++;
        swap(arr,small,end);
        return  small;
    }
    public static void swap(int[] arr,int index1,int index2){
        int tmp=arr[index1];
        arr[index1]=arr[index2];
        arr[index2]=tmp;
    }
}

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章