算法学习(5):O(NlogN)的排序

O(NlogN)的排序

master公式

T(N) = aT(N/b) + O(N^d)

  • b:子过程的样本量
  • a:子过程的计算次数
  • O(N^d):除了递归之外操作的时间复杂度

满足如上公式的程序都可以根据master公式计算时间复杂度:

  • log(b,a) > d :时间复杂度为O(N^log(b,a))
  • log(b,a) = d :时间复杂度为O(N^d * logN)
  • log(b,a) < d :时间复杂度为O(N^d)

归并排序

归并排序的基本思想

归并排序的整体就是一个递归的过程,左边排好序,右边排好序,再让其整体有序

归并排序的C++代码实现

void process(vector<int>& arr, int left, int right);

void merge(vector<int>& arr, int left, int mid, int right);

void mergeSort(vector<int> &arr)
{
    if (arr.size() < 2)
    {
        return;
    }
    process(arr, 0, arr.size() - 1);
}

void process(vector<int>& arr, int left, int right)
{
    if (left == right)
        return;
    int mid = left + (right - left) / 2;
    process(arr, left, mid);
    process(arr, mid + 1, right);
    merge(arr, left, mid, right);
}

void merge(vector<int>& arr, int left, int mid, int right)
{
    vector<int> help;
    int p1 = left;
    int p2 = mid + 1;
    while(p1 <= mid && p2 <= right)
        if (arr[p1] <= arr[p2])
            help.push_back(arr[p1++]);
        else
            help.push_back(arr[p2++]);
    while (p1 <= mid)
        help.push_back(arr[p1++]);
    while (p2 <= right)
        help.push_back(arr[p2++]);
    for (int i = 0; i < help.size(); i++)
        arr[left + i] = help[i];

}

利用master公式求解归并排序的时间复杂度

T(N) = 2 * T(N/2) + O(N)

  • a = 2,因为process函数里调用了两次自身
  • b = 2,因为每次分二分之一
  • d = 1,因为merge函数的时间复杂度为O(N)

log(2, 2) = 1 = d,所以归并排序的时间复杂度是O(N*logN)

归并排序的额外空间复杂度

O(N),可以最多准备长度为N的额外空间给merge的过程重复使用,每次使用完释放。

归并排序算法题

(1)小和问题:在一个数组中,每一个数左边比当前数小的数累加起来名叫作这个数组的小和。求一个数组的小和。
例子:[1,3,4,2,5]

  • 1左边比1小的数,没有;
  • 3左边比3小的数,1;
  • 4左边比四小的数,1、3;
  • 2左边比2小的数,1;
  • 5左边比5小的数,1、3、4、2;
  • 所以小和为1+1+3+1+1+3+4+2=16

解题思路:将小和的求法由左边比当前数小转化为右边比当前数大,右边有几个比自身大的数,就有几个自身的小和。
例如:[1,3,4,2,5]

  • 1右边比1大的数,3、4、2、5,四个,所以有4个1的小和;
  • 3右边比3大的数,4、5,两个,所以有2个3的小和;
  • 4右边比四大的数,5,一个,所以有1个4的小和;
  • 2右边比2大的数,5,一个,所以有1个2的小和;
  • 5右边比5大的数,没有;
  • 所以小和为1+1+1+1+3+3+4+2=16。

然后利用归并排序的思想,从中间分割一直往下,左边最下层1、3,左边的数与右边的数依次比较,如果右边有比左边的数大的,就累加一个左边小的数,依次往上回溯,通俗来讲就是比归并排序在merge函数中多了个累加小和的过程,并且归并排序的merge函数默认左右两边相等时拷贝左边的数字,但是小和问题中要拷贝右边的数字,仔细解释起来比较麻烦,详细算法思想见https://www.bilibili.com/video/BV13g41157hK?p=4&vd_source=77d06bb648c4cce91c6939baa0595bcd P4 1:01:37开始。
C++代码实现:

int ssProcess(vector<int>& arr, int left, int right);

int ssMerge(vector<int>& arr, int left, int mid, int right);

int smallSum(vector<int>& arr)
{
    if (arr.size() < 2)
    {
        return 0;
    }
    return ssProcess(arr, 0, arr.size() - 1);
}

int ssProcess(vector<int>& arr, int left, int right)
{
    if (left == right)
        return 0;
    int mid = left + (right - left) / 2;
    return ssProcess(arr, left, mid) + ssProcess(arr, mid + 1, right) + ssMerge(arr, left, mid, right);
}

int ssMerge(vector<int>& arr, int left, int mid, int right)
{
    vector<int> help;
    int p1 = left;
    int p2 = mid + 1;
    int res = 0;
    while (p1 <= mid && p2 <= right)
    {
        if (arr[p1] < arr[p2])
        {
            res += (right - p2 + 1) * arr[p1];
            help.push_back(arr[p1++]);
        }
        else
            help.push_back(arr[p2++]);
    }
    while (p1 <= mid)
        help.push_back(arr[p1++]);
    while (p2 <= right)
        help.push_back(arr[p2++]);
    for (int i = 0; i < help.size(); i++)
        arr[left + i] = help[i];
    return res;
}

(2)逆序对问题:在一个数组中,左边的数如果比右边的数大,则这两个数构成一个逆序对,请打印所有逆序对。
解题思路:与上面小和问题的大致思路一样,小和问题求的是比右边的数小的情况,这里是反过来了。注意一点,这里排序需要从大到小排。
C++代码实现:

void ROPProcess(vector<int>& arr, int left, int right);

void ROPMerge(vector<int>& arr, int left, int mid, int right);

void reverseOrderPair(vector<int>& arr)
{
    if (arr.size() < 2)
    {
        return;
    }
    ROPProcess(arr, 0, arr.size() - 1);
}

void ROPProcess(vector<int>& arr, int left, int right)
{
    if (left == right)
        return;
    int mid = left + (right - left) / 2;
    ROPProcess(arr, left, mid);
    ROPProcess(arr, mid + 1, right);
    ROPMerge(arr, left, mid, right);
}

void ROPMerge(vector<int>& arr, int left, int mid, int right)
{
    vector<int> help;
    int p1 = left;
    int p2 = mid + 1;
    while (p1 <= mid && p2 <= right)
    {
        if (arr[p1] > arr[p2])
        {
            cout << "(" << arr[p1] << ", " << arr[p2] << ")" << " ";
            help.push_back(arr[p1++]);
        }
        else
            help.push_back(arr[p2++]);
    }
    while (p1 <= mid)
        help.push_back(arr[p1++]);
    while (p2 <= right)
        help.push_back(arr[p2++]);
    for (int i = 0; i < help.size(); i++)
        arr[left + i] = help[i];
}

快速排序

荷兰国旗问题

(1)给定一个数组arr和一个数num,请把小于等于num的数放在数组的左边,大于num的数放在数组的右边。要求额外空间复杂度O(1),时间复杂度O(N)。
解题思路:双指针法,一个指针i指向当前数,另一个指针L指向排好的小于等于num的数的最后一个。

i移动:

  • arr[i] <= num时,arr[i]与L指针指向的数字的下一个数即arr[L+1]交换,L++,i++;
  • arr[i] > num时,i++。

当i越界时停止。

详细算法思路解释见https://www.bilibili.com/video/BV13g41157hKp=4&vd_source=77d06bb648c4cce91c6939baa0595bcd P4 1:45:40 开始
C++代码实现:

void doubleColor(vector<int>& arr, int num)
{
    int i = 0;
    int L = -1;
    for (; i < arr.size(); i++)
    {
        if (arr[i] <= num)
        {
            swap(arr, i, L + 1); //交换数组arr中下标为i和L+1的数,往期文章中提到过,这里不再作实现
            L++;
        }
    }
}

(2)(荷兰国旗问题)给定一个数组arr和一个数num,请把小于num的数放在数组的左边,等于num的数放在数组的中间,大于num的数放在数组的右边。要求额外空间复杂度O(1),时间复杂度O(N)。
解题思路:与上一题的思路类似,三指针法,一个指针i指向当前数,一个指针L指向排好的小于num的数的最后一个,另一个指针R指向排好的大于num的数的最前面一个。

i移动:

  • arr[i] < num时,arr[i]与L指针指向的数字的下一个数即arr[L+1]交换,L++,i++;
  • arr[i] = num时,i++;
  • arr[i] > num时,arr[i]与R指针指向的数字的前一个数即arr[R-1]交换,R--,i不变。

当i等于R时停止。

详细算法思路解释见https://www.bilibili.com/video/BV13g41157hKp=4&vd_source=77d06bb648c4cce91c6939baa0595bcd P4 1:53:50
C++代码实现:

void tripleColor(vector<int>& arr, int num)
{
    int i = 0;
    int L = -1;
    int R = arr.size();
    while (i < R)
    {
        if (arr[i] < num)
        {
            swap(arr, i, L + 1);
            L++;
            i++;
        }
        else if (arr[i] == num)
            i++;
        else
        {
            swap(arr, i, R - 1);
            R--;
        }
    }
}

快速排序1.0版本

思路:荷兰国旗第一问思路,将给定的num改为数组最后一个数字,小于等于num的放在数组左边,大于num的放在数组右边,然后将数组分为两部分,左边部分从下标为0的数开始,到小于等于num的最后一个数结束。右边部分从大于num的第一个数开始,到数组的最后一个数结束。然后对这两部分数组递归,重复前面的操作。

快速排序2.0版本

思路:荷兰国旗第二问思路,将给定的num改为数组最后一个数字,小于num的放在数组左边,等于num的放在数组中间,大于num的放在数组右边,然后将数组分为三部分,左边部分从下标为0的数开始,到小于num的最后一个数结束。中间部分为等于num的数。右边部分从大于num的第一个数开始,到数组的最后一个数结束。然后对这左右两部分数组递归,中间部分不动,重复前面的操作。


由于2.0版本中间不用动的是等于num的数,每次递归搞定一这批数,所以比1.0版本要好。但是由于选择的数字不知道大小,最好的情况是选到的数字刚好是数组的最中间的数,递归的两边长度相等,这时利用master公式算出的时间复杂度是O(NlogN);最坏的情况是数组已经排好序,最后一个数是最大的数,每次递归左半部分为当前数组长度-1,右半部分为0,利用master公式算出的时间复杂度就是 O(N2),由于算法的时间复杂度是最差的情况,所以整个算法的时间复杂度就是O(N2)。这种时间复杂度太高了,于是快速排序3.0版本应运而生

快速排序3.0版本

在快速排序2.0版本的基础上,只改动了一点:从原来的从数组最后一个数取num改为从数组中随机取num,这样,每次取得的数大小是随机的,有时好有时坏,这种情况利用master公式算出时间复杂度的长期期望是O(NlogN),即整个算法的时间复杂度是O(NlogN)。

快速排序3.0版本C++代码实现

void QSProcess(vector<int>& arr, int left, int right);

vector<int> partition(vector<int>& arr, int left, int right);

void queckSort(vector<int> &arr)
{
    if (arr.size() < 2)
    {
        return;
    }
    QSProcess(arr, 0, arr.size() - 1);
}

void QSProcess(vector<int>& arr, int left, int right)
{
    if (left < right)
    {
        srand((unsigned)time(NULL));
        int pos = (rand() % (right - left + 1)) + left; //随机选择一个数组中的下标
        swap(arr, pos, right); //将选中的下标与数组中的最后一个数交换
        vector<int> p = partition(arr, left, right);
        QSProcess(arr, left, p[0] - 1);
        QSProcess(arr, p[1] + 1, right);
    }
}

vector<int> partition(vector<int>& arr, int left, int right)
{
    int i = left;
    int p1 = left - 1;
    int p2 = right + 1;
    int num = arr[right];
    while (i < p2)
    {
        if (arr[i] < num)
        {
            swap(arr, i++, ++p1);
        }
        else if(arr[i] > num)
        {
            swap(arr, i, --p2);
        }
        else
        {
            i++;
        }
    }
    return { p1 + 1,p2 - 1 };
}

快速排序的额外空间复杂度

与时间复杂度类似,概率累加是O(logN)。

堆排序

大根堆,小根堆

大根堆:所有的父亲节点都比孩子节点大
小根堆:所有的父亲节点都比孩子节点小

堆排序算法思想

首先堆的大小heapSize定义为数组大小,然后将heapSize代表的堆变为大根堆,之后将堆的第一个数(最大的数)与最后一个数交换位置,heapSize减一,然后重复前面的操作,周而复始。让堆变为大根堆的操作heapInsert和heapIfy的算法思想见https://www.bilibili.com/video/BV13g41157hKp=5&vd_source=77d06bb648c4cce91c6939baa0595bcd P5 0:18:48

堆排序C++代码实现

void heapInsert(vector<int>& arr, int index);

void heapIfy(vector<int>& arr, int index, int heapSize);

void heapSort(vector<int>& arr)
{
    if (arr.size() < 2)
    {
        return;
    }
    for (int i = 0; i < arr.size(); i++)
    {
        heapInsert(arr, i);
    }
    int heapSize = arr.size();
    swap(arr, 0, --heapSize);
    while (heapSize > 0)
    {
        heapIfy(arr, 0, heapSize);
        swap(arr, 0, --heapSize);
    }
}

void heapInsert(vector<int>& arr, int index)
{
    while (arr[index] > arr[(index - 1) / 2])
    {
        swap(arr, index, (index - 1) / 2);
        index = (index - 1) / 2;
    }
}

void heapIfy(vector<int>& arr, int index, int heapSize)
{
    int left = 2 * index + 1;
    while (left < heapSize)
    {
        int largest = left + 1 < heapSize && arr[left] < arr[left + 1] ? left + 1 : left;
        largest = arr[index] > arr[largest] ? index : largest;
        if (largest == index)
            break;
        swap(arr, index, largest);
        index = largest;
        left = 2 * index + 1;
    }
}
posted @ 2022-07-20 12:58  小肉包i  阅读(61)  评论(0)    收藏  举报