排序算法(二):时间复杂度为O(nlogn)的排序算法

一、希尔排序(实际上很少用到,了解即可)

1、算法步骤

  1. 将待排序数组按照一定的间隔进行排序,如此时排序间隔为gap:则从index=gap处的元素开始排序;对于index = i的元素,每次和当前index - gap处的元素进行比较,直到符合插入条件;然后继续比较index = i++的元素,直到数组末尾,完成当前间隔的排序
  2. 逐渐缩小间隔进行下一轮排序
  3. 最后一轮时,取间隔为 1,也就相当于直接使用插入排序。但这时经过前面的「宏观调控」,数组已经基本有序了,所以此时的插入排序只需进行少量交换便可完成

其中,每一遍排序的间隔在希尔排序中被称之为增量,所有的增量组成的序列称之为增量序列, 上图中增量序列为 [5, 2, 1]。增量依次递减,最后一个增量必须为 1

有一条非常重要的性质保证了希尔排序的效率:D(K+1)间隔有序的序列在经过D(K)间隔排序后仍然是D(K+1)间隔有序的,其中D(K+1) >= D(K)

2、算法实现

void shellSort(vector<int>& arr){
	//排序间隔循环
    for(int gap = arr.size() / 2; gap > 0; gap /= 2){

		/-------以下代码其实就是插入排序-----------/
    	//使用当前间隔进行排序,元素范围为`[gap, size()-1)
        for(int i = gap; i < arr.size(); i++){
        
        	//记录当前待插入元素 和 第一个被比较的元素
            int curNum = arr[i];
            int j = i - gap;
            
            //如果没有比较到首元素 或 带插入元素仍小于当前比较元素,继续比较
            while(j >= 0 && arr[j] > curNum){
                arr[j + gap] = arr[j];
                j -= gap;
            }

			//找到了插入位置j+gap(这里加gap是抵消最后一次-gap)
            arr[j + gap] = curNum;
        }
    }
}

3、算法性能

排序方式:in-place

希尔排序稳定性:不稳定

希尔排序时间复杂度:与增量序列有关,O(n)到O(n^2)之间,最好为O(n^1.3)

希尔排序空间复杂度:O(1)

4、增量序列的选择

1)Knuth增量序列(D1 = 1,Dk+1 = 3Dk + 1)

平均时间复杂度为O(n^1.5)

2)Hibbard增量序列(Dk = 2^k - 1)

最坏时间复杂度O(n^1.5),平均时间复杂度O(n^1.25)

3)Sedgewick增量序列(94^k - 92^k + 1 和 4^k - 3*2^k + 1序列交替构成)

最坏时间复杂度O(n^(4/3)),平均时间复杂度O(n^(7/6))

5、希尔排序算法与O(n^2)级别算法的本质区别

逆序对

当我们从小到大排序时,在数组中的两个数字,如果前面一个数字大于后面的数字,则这两个数字组成一个逆序对

希尔排序突破O(n^2)的关键

对于随机数组,逆序对的数量是 O(n^2)级的,如果采用「交换相邻元素」的办法来消除逆序对,每次最多只能消除一组逆序对,因此必须执行 O(n^2)级的交换次数,这就是为什么冒泡、插入、选择算法只能到 O(n^2)级的原因。反过来说,基于交换元素的排序算法要想突破 O(n^2) 级,必须通过一些比较,交换间隔比较远的元素,使得一次交换能消除一个以上的逆序对。

二、堆排序(用的也比较少,快速排序比对排序要快)

大顶堆与小顶堆

堆是符合以下两个条件之一的完全二叉树:

  • 根节点的值 ≥ 子节点的值,这样的堆被称之为最大堆,或大顶堆;
  • 根节点的值 ≤ 子节点的值,这样的堆被称之为最小堆,或小顶堆。

1、算法步骤

  1. 用数列构建出一个大顶堆,取出堆顶的数字;
  2. 调整剩余的数字,构建出新的大顶堆,再次取出堆顶的数字;
  3. 循环往复,完成整个排序。

2、算法实现

对于按照层次遍历记录成数组的完全二叉树,将根节点的下标记录为0,则有以下性质:

  1. 对于完全二叉树中的第 i 个数,它的左子节点下标:left = 2i + 1
  2. 对于完全二叉树中的第 i 个数,它的右子节点下标:right = left + 1
  3. 对于有 n 个元素的完全二叉树(n≥2),它的最后一个非叶子结点的下标:n/2 - 1
//调整数组中的index = i的元素,使该元素的位置满足最大堆,其中heapSize是当前需要考虑的最大堆的元素数量
void maxHeapAdjustI(vector<int>& arr, int i, int heapSize){
	//获取该元素的左右孩子坐标
    int left = 2 * i + 1;
    int right = left + 1;
	
	//maxIndex为该元素与其左右孩子中最大值的index
    int maxIndex = i;
    if(left < heapSize && arr[left] > arr[maxIndex]) maxIndex = left;
    if(right < heapSize && arr[right] > arr[maxIndex]) maxIndex = right;

	//如果最大值不是该元素
    if(maxIndex != i){
    	//将该元素与最大值交换
        swap(arr[i], arr[maxIndex]);
        //并继续调整该元素使其满足最大堆(交换后该元素的index从i变成了maxIndex)
        maxHeapAdjustI(arr, maxIndex, heapSize);
    }
}

//初始化最大堆
void buildMaxHeap(vector<int>& arr){
	//针对最大堆中的每个非叶子节点,调整其位置使其满足最大堆
    for(int index = arr.size() / 2 - 1; index >= 0; index--){
        maxHeapAdjustI(arr, index, arr.size() - 1);
    }
}

//排序主函数
void heapSort(vector<int>& arr){
	//初始化最大堆
    buildMaxHeap(arr);
    //将最大堆根元素放到数组最后面,然后重新组织最大堆
    for(int i = arr.size() - 1; i > 0; i--){
    	//将最大堆根元素放到最后面
        swap(arr[0], arr[i]);
        //由于最后一个元素被交换到index=0的位置上,因此需要针对该元素重新调整最大堆
        maxHeapAdjustI(arr, 0, i);
    }
}

3、算法性能

排序方式:in-place

堆排序稳定性:不稳定

堆排序时间复杂度:

堆初始化时间O(n),之后维护堆的总时间O(nlogn),总时间复杂度O(nlogn)

堆排序空间复杂度:O(1)

三、快速排序(在O(nlogn)级算法中多数效率更高,应用广泛,面试常考,但其不稳定)

1、算法步骤

  1. 从数组中取出一个数,称之为基数
  2. 遍历数组,将比基数大的数字放到它的右边,比基数小的数字放到它的左边。遍历完成后,数组被分成了左右两个区域
  3. 将左右两个区域视为两个数组,重复前两个步骤,直到排序完成

按照上述规则,每轮中的基数都被调整到其最终的位置上:第一轮遍历排好 1 个基数,第二轮遍历排好 2 个基数(两个子数组中有两个基数),第三轮遍历排好 4 个基数(四个子数组中有四个基数)… 因此总遍历轮数为logn ~ n 次

2、基数的选择

通常来讲有三种选择方式:

  1. 选择第一个元素作为基数
  2. 选择最后一个元素作为基数
  3. 选择区间内一个随机元素作为基数

其中选择区间内一个随机元素作为基数平均时间复杂度是最优的

  • 而当数组本身就是顺序或者逆序的,此时选择第一个元素或最后一个元素为基数会导致每次轮分区后,都有一个分区是空的,这导致每轮只能排序一个基数,从而使得排序的时间复杂度达到了O(n^2)级别;而选择区间内一个随机元素作为基数则可以很好的避免这一情况

2、算法实现

注意:这里数组使用的是双闭区间

/-----使用递归函数实现排序-----/
void quickSort(vector<int>& arr, int beg, int end){

	//递归终止条件:当前分数组中只有1个或没有元素时,退出递归
    if(beg >= end) return;

	/----双指针法对进行本轮分区,left和right分别指向开始和结尾-----/
    int left = beg, right = end;

	/-----当指针没相遇的时候,不断寻找大于基数和小于基数的对,交换他们-----/
    while(left < right){
        while(left < right && arr[right] >= arr[beg]) right--; //保证指针相遇的时候及时退出,一定要先找右边小于基数的元素,这样退出的时候,left和right才会指向小于基数的元素
        while(left < right && arr[left] <= arr[beg]) left++;
        swap(arr[left], arr[right]);  //找到这样的一对数,交换他们
    }

	//当指针相遇时,先找右边小于基数的元素,因此此时指针指向小于等于基数的元素
	//因此交换基数和当前指针位置即完成数组分区
    swap(arr[beg], arr[left]);

	/-----迭代处理左数组和右数组
    quickSort(arr, beg, left - 1);
    quickSort(arr, left + 1, end);
}

3、算法性能

快速排序方式:in-place

快速排序稳定性:不稳定

快速排序时间复杂度:最坏时间复杂度O(n^2),平均时间复杂度O(nlogn)

快速排序空间复杂度:O(logn),主要来源于递归

四、归并排序(针对两个有序数组)

1、算法步骤

开辟一个长度等同于两个数组长度之和的新数组,并使用两个指针来遍历原有的两个数组,不断将较小的数字添加到新数组中,并移动对应的指针即可。

2、算法实现

vector<int> mergeSort(vector<int> arr1, vector<int> arr2){
	/---算法初始化---/
    vector<int> arr(arr1.size() + arr2.size());   //开辟新数组
    int index1 = 0, index2 = 0;   //初始化遍历下标
    
    /---当两个有序数组均存在元素时候---/
    while(index1 < arr1.size() && index2 < arr2.size()){
    	/---哪个数组的元素小从哪个数组中取出来一个---/
        if(arr1[index1] <= arr2[index2]){   
            arr[index1 + index2] = arr1[index1];
            index1++;
        } else {
            arr[index1 + index2] = arr2[index2];
            index2++;
        }
    }

	/---如果其中一个数组没了,就从剩下的数组直接补上---/
    while(index1 < arr1.size()){
        arr[index1 + index2] = arr1[index1];
        index1++;
    }

    while(index2 < arr2.size()) {
        arr[index1 + index2] = arr2[index2];
        index2++;
    }

    return arr;  //返回结果
}

3、算法性能

快速排序方式:out-place

快速排序稳定性:稳定

快速排序时间复杂度:时间复杂度O(m+n)

快速排序空间复杂度:O(m+n)

五、归并排序(针对一个无序数组)

1、算法步骤

我们可以把数组不断地拆成两份,直到只剩下一个数字时,这一个数字组成的数组我们就可以认为它是有序的,两个对于两个由一个数字组成的数组我们就可以使用归并排序得到一个有两个数字的有序数组;然后再将这些拆分的数组不断的两两组合起来,就完成了归并排序。

二、算法实现

注意:这里数组使用的是双闭区间

/-----使用归并排序原地排序两个有序数组-----/
void merge(vector<int>& arr, int beg, int end) {

	//两个数组分别是当前数组的前半部分和后半部分
    int index1 = beg;
    int end1 = (beg + end) / 2;
    int index2 = end1 + 1;

	//----原地归并排序---/
    while (index1 <= end1 && index2 <= end) {

		//一次查找左数组中大于右数组的元素,交换两者,让小的到左数组中
		//这里是大于而不是小于等于,这是有序的关键
        if (arr[index1] > arr[index2]) {
            swap(arr[index1], arr[index2]);

			//被交换到右数组中的较大者仍需要向后查找,知道找到>=自己的元素,坐在该元素的前面
			//这里找到>=自己的元素,是算法有序的关键
            if (index2 != end) {
                int curNum = arr[index2];
                int curIndex = index2;
                while (curIndex < end && arr[curIndex + 1] < curNum) {
                    arr[curIndex] = arr[curIndex + 1];
                    curIndex++;
                }
                arr[curIndex] = curNum;
            }
        }
        index1++;
    }
}

/-----递归调用归并排序对数组进行排序----/
void mergeSort(vector<int>& arr, int beg, int end){
	//递归终止条件:当数组中只有一个元素的时候,停止递归
    if(beg == end) return;

	//将当前数组分割为左右两个子数组分别排序
    int middle = (beg + end) / 2;
    mergeSort(arr, beg, middle);
    mergeSort(arr, middle + 1, end);
    
    //调用归并排序对两个有序数组进行排序
    merge(arr, beg, end);
}

三、算法性能

快速排序方式:out-place(也可以实现in-place)

快速排序稳定性:稳定

快速排序时间复杂度:时间复杂度O(nlogn)

快速排序空间复杂度:out-place方式为O(m+n),in-place方式为O(1)

你可能感兴趣的:(#,排序算法,数据结构与算法,二叉树,数据结构)