歸並排序,快速排序,堆排序實現及復雜度分析


1. 算法實現

排序中比較復雜的有歸並排序,快速排序,堆排序三大算法了,三個算法的時間復雜度都是O(N * logN),三個算法的思想我就簡單的展開詳述以下。

1.1 歸並排序

歸並排序的核心思想是鏈表中的經典題目:合並兩個有序鏈表

劍指offer:合並兩個排序的鏈表

Leetcode P21: Merge Two Sorted Lists

采用分治的思想,將整個數組分為兩個部分,先對左邊的數組進行歸並排序,再對右邊的數組進行歸並排序,最后兩者進行merge

下面的函數就是歸並排序的歸並部分,將兩個已經有序的數組歸並。

public class MergeSort implements Sort{
    @Override
    public void sort(int[] arr) {
        if (arr == null || arr.length <= 1) return;
        mergeSort(arr, 0, arr.length - 1);
    }
    public void mergeSort(int[] arr, int l, int r) {
        if (l >= r) return;
        int mid = l + ((r - l) >> 1);
        //注意遞歸是i
        mergeSort(arr, l, mid);
        mergeSort(arr, mid + 1, r);
        merge(arr, l, mid, r);
    }
    public void merge(int[] arr, int l, int mid, int r) {
        int[] newArr = new int[r - l + 1];
        int left = l;
        int right = mid + 1;
        int index = 0;
        while (left <= mid && right <= r) {
            if (arr[left] > arr[right]) {
                newArr[index++] = arr[right++];
            } else {
                newArr[index++] = arr[left++];
            }
        }
        while (left <= mid) {
            newArr[index++] = arr[left++];
        }
        while (right <= r) {
            newArr[index++] = arr[right++];
        }
        for (int i = 0; i < newArr.length; i++) {
            arr[i + l] = newArr[i];
        }
    }
}

1.2 堆排序

堆排序首先就要利用堆這個數據結構。首先先將整個數組結構調整成堆。

heapInset函數插入某個數字,並且將插入的部分進行調整。對於一個數組,從位置0開始插入,並且邊插入邊進行調整。

public class HeapSort implements Sort{
    @Override
    public void sort(int[] arr) {
        if (arr == null || arr.length <= 1) return;
        for (int i = 0; i < arr.length; i++) {
            heapInsert(arr, i);
        }
        int heapSize = arr.length;
        swap(arr, 0, --heapSize);
        while (heapSize > 0) {
            heapify(arr, 0, heapSize);
            swap(arr, 0, --heapSize);
        }
    }
    public void heapInsert(int[] arr, int index) {
        //index > 0 的條件可以省略,因為如果index是1的話,兩個值必定相等,無法進入循環
        while (arr[(index - 1) / 2] < arr[index]) {
            int fa = (index - 1) / 2;
            swap(arr, index, fa);
            index = fa;
        }
    }
    public void heapify(int[]arr, int index, int heapSize) {
        int left = index * 2 + 1;
        while (left < heapSize) {
            int right = left + 1;
            int maxLeftRightIndex = right < heapSize && arr[left] < arr[right] ? right : left;
            int maxIndex = arr[maxLeftRightIndex] > arr[index] ? maxLeftRightIndex : index;
            if (maxIndex == index) {
                break;
            }
            
            swap(arr, index, maxIndex);
            index = maxIndex;
            left = maxIndex * 2 + 1;
        }
    }
    public void swap(int []arr, int i, int j) {
        int temp = arr[i];
        arr[i] = arr[j];
        arr[j] = temp;
    }
}

這樣處理完之后,數組就成為了一個最大堆。要將數組調整為有序的,要經歷以下幾步:

  1. 數組的第一個數是最大堆的頂,是最大的數,每次將第一個數和最大堆的最后一個數交換。這樣后面部分就排好序了
  2. 交換完之后,最大堆的結構就會被破壞了,所以需要調整最大堆。
//交換到最后一個位置
swap(arr, 0, --heapSize);

while (heapSize > 0) {
    //調整位置成最大堆
    heapify(arr, 0, heapSize);
    //之后再次進行交換
    swap(arr, 0, --heapSize);
}

調整位置

public void heapify(int[]arr, int index, int heapSize) {
    int left = index * 2 + 1;
    while (left < heapSize) {
        int right = left + 1;
        int maxLeftRightIndex = right < heapSize && arr[left] < arr[right] ? right : left;
        int maxIndex = arr[maxLeftRightIndex] > arr[index] ? maxLeftRightIndex : index;
        if (maxIndex == index) {
            break;
        }

        swap(arr, index, maxIndex);
        index = maxIndex;
        left = maxIndex * 2 + 1;
    }
}

1.3 快速排序

快速排序的核心思想思想是選一個數,不管是隨機的還是固定的,每一步將大於這個數的數字放在數組的右邊,小於的所有數字放在左邊,聽起來是不是很熟悉,這就是經典題目荷蘭國旗的思想啦。

Leetcode P75: Sort Colors

public class QuickSort implements Sort{
    @Override
    public void sort(int[] arr) {
        if (arr == null || arr.length <= 1) return;
        int length = arr.length;
        quickSort(arr, 0, length - 1);
    }
    public void quickSort(int[] arr, int l, int r) {
        if (l >= r || l < 0) return;
        int[] mids = partition(arr, l, r);
        quickSort(arr, l, mids[0]);
        quickSort(arr, mids[1], r);
    }
    public int[] partition(int arr[], int l, int r) {
        int[] mids = new int[]{l ,r};
        int num = arr[r];
        int left = l - 1;
        int right = r;
        int cur = l;
        while (cur < right) {
            if (arr[cur] < num) {
                swap(arr, ++left, cur++);
            } else if (arr[cur] > num) {
                swap(arr, --right, cur);
            } else {
                cur++;
            }
        }
        swap(arr, right++, r);
        mids[0] = left;
        mids[1] = right;
        return mids;
    }
    public void swap(int[] arr, int i, int j) {
        int temp = arr[i];
        arr[i] = arr[j];
        arr[j] = temp;
    }
    public static void main(String[] args) {
        int[] arr = new int[]{4,1,2,3,4,3,1,4,1,1,23,4,5,4,2,2,3,5,3,1,1,2,3,4,5,6,7};
        QuickSort q = new QuickSort();
        q.sort(arr);
    }
}

2. 時間復雜度分析

2.1 Master theorem

Master公式是為了評估遞歸函數的復雜度而誕生的。

T(N) = a* T(N / 2) + O(N^d)

  1. log(b,a) > d時,時間復雜度是O(N ^ log(b,a))
  2. log(b,a) = d時,時間復雜度是O(N ^ d + logN)
  3. log(b,a) < d時,時間復雜度是O(N^d)

2.2 算法復雜度分析

使用master公式計算三個排序的時間復雜度

2.2.1 歸並排序

Merge-Sort的核心代碼如下:

    public void mergeSort(int[] arr, int l, int r) {
        if (l >= r) return;

        int mid = l + ((r - l) >> 1);
				
	    (1)part 1
        mergeSort(arr, l, mid);
        mergeSort(arr, mid + 1, r);
				
	    (2)part 2
        merge(arr, l, mid, r);
    }

時間復雜度估算:

  1. part 1部分將問題分為兩個部分,所以mastera=2,b=2
  2. part 2部分是有序鏈表的合並,時間復雜度是:O(N),所以d=1

所以套用master公式中的第二條,時間復雜度是O(N*logN)

2.2.2 堆排序

Merge-Sort的核心代碼如下:

public void sort(int[] arr) {
    if (arr == null || arr.length <= 1) return;
    
    (1)part1
    for (int i = 0; i < arr.length; i++) {
        heapInsert(arr, i);
    }
    int heapSize = arr.length;
    
    (2)part2
    swap(arr, 0, --heapSize);
    while (heapSize > 0) {
        heapify(arr, 0, heapSize);
        swap(arr, 0, --heapSize);
    }
}

時間復雜度估算:

  1. 最大堆的插入后調整的時間復雜度是O(logN)part 1部分是插入N個元素,時間復雜度是log1+log2+log3+log4+...+logN = N,所以part 1的時間復雜度是O(N)
  2. part 2部分中heapify的時間復雜度是logNheapify就是堆的調整,所以part 2部分的時間復雜度是O(N * logN)
  3. 加起來就是O(N)+O(N*logN),取最大項即為O(N*logN)

2.2.3 快速排序

Quick-Sort的核心代碼如下:

public void quickSort(int[] arr, int l, int r) {
    if (l >= r || l < 0) return;
    (1)part 1
    int[] mids = partition(arr, l, r);
    
    (2)part 2
    quickSort(arr, l, mids[0]);
    quickSort(arr, mids[1], r);
}

時間復雜度估算:

  1. part 1中的partition思想是荷蘭國旗問題,時間復雜度是O(N)
  2. part 2和歸並排序一樣,也是將問題分為了兩個部分,所以a=2,b=2

所以快速排序的時間復雜度是O(N*logN)

2.2.4 空間復雜度

除了歸並排序中,需要提前分配O(N)的空間,作為Merge過程中的緩存。堆排序和快速排序全部都是in-place修改的,所以不需要分配額外空間。

其實在在歸並排序和堆排序過程中,因為函數遞歸,函數占用的棧地址也算空間復雜度,但是為什么沒有算進去呢?個人認為是因為在一些高手的眼中,遞歸函數是可以改成循環,所以這就不用考慮這些額外空間了。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM