leetcode总结无止境系列之排序

几种排序算法的时间和空间效率对比: 稳定排序

  • 冒泡排序(bubble sort) O(n平方)
  • 插入排序 (insertion sort)O(n平方)
  • 桶排序 (bucket sort)O(n); 需要 O(n)额外空间
  • 计数排序 (counting sort) O(n+k); 需要O(n+k)额外空间
  • 合并排序 (merge sort)O(n log n); 需要 O(n) 额外空间
  • 二叉排序树排序 (Binary tree sort) O(nlogn)-期望时间; O(n平方)-最坏时间; 需要O(n)额外空间
  • 基数排序 (radix sort)O(n*k); 需要O(n)额外空间

不稳定排序

  • 选择排序 (selection sort)O(n平方)
  • 希尔排序 (shell sort)O(nlogn) 如果使用最佳的现在版本
  • 堆排序 (heapsort)O(nlogn)
  • 快速排序 (quicksort)O(nlogn)-期望时间, O(n平方)-最坏情况; 对于大的、乱数串行一般相信是最快的已知排序

主流排序算法中考查得最多的是快排+归并排序

关于冒泡

以从小到大排序举例:设数组长度为N

1.比较相邻的前后二个数据,如果前面数据大于后面的数据,就将二个数据交换。

2.这样对数组的第0个数据到N-1个数据进行一次遍历后,最大的一个数据就"沉"到数组第N-1个位置。

3.N=N-1,如果N不为0就重复前面二步,否则排序完成。

void BubbleSort(int a[], int n)
{
   int i, j;
   for (i = 0; i < n; i++)
    for (j = 0; j < n - i - 1; j++)
     if (a[j] > a[j + 1])
        Swap(a[j], a[j + 1]);
}
关于插入排序

设数组为a[0...n-1]。

1.初始时,a[0]自成1个有序区,无序区为a[1..n-1]。令i=1

2.将a[i]并入当前的有序区a[0...i-1]中形成a[0...i]的有序区间。

3.i++并重复第二步直到i==n-1。排序完成。

void Insertsort3(int a[], int n)
{
    int i, j;
    for (i = 1; i < n; i++)
    for (j = i; j >= 0; j--)
        if(a[j] < a[j - 1])
             Swap(a[j], a[j - 1]);
}
关于选择排序

直接选择排序和直接插入排序类似,都将数据分为有序区和无序区,所不同的是直接插入排序是将无序区的第一个元素直接插入到有序区以形成一个更大的有序区,而直接选择排序是从无序区选一个最小的元素直接放到有序区的最后。 设数组为a[0...n-1]。 1.初始时,数组全为无序区为a[0..n-1]。令i=0

2.在无序区a[i...n-1]中选取一个最小的元素,将其与a[i]交换。交换之后a[0...i]就形成了一个有序区。

3.i++并重复第二步直到i==n-1。排序完成。

void Selectsort(int a[], int n)
{
    int i, j, nMinIndex;
    for (i = 0; i < n; i++)
     {
        nMinIndex = i; //找最小元素的位置
        for (j = i + 1; j < n; j++)
         if (a[j] < a[nMinIndex])
            nMinIndex = j;  
            Swap(a[i], a[nMinIndex]); //将这个元素放到无序区的开头
    }
}
关于快排

应用场合:排序、topK的查找 C++实现代码:

int RandomInRange(int start, int end)
{
    return rand() % (end - start + 1);
}

int partition(int data[], int length, int start, int end)
{
    if(data == NULL || length <= 0 || start <0 || end >= length)
        throw new exception("Invalid Parameters");
    int index = RandomInRange(start, end);
    swap(&data[index], &data[end]);
    int small = start - 1;
    for(int index = start; index <= end; index++)
    {
        if(data[index] < data[end])
        {
            small++;
            if(small != index)
            swap(&data[small], &data[index]);
        }
    }
    small++;
    swap(&data[small], &data[end]);
    return small;
}

void QuickSort(int data[], int length, int start, int end)
{
    if(start == end)
        return;
    if(start < end)
    {
        int index = partition(data, length, start, end);
        QuickSort(data, length, start, index - 1);
        QuickSort(data, length, index + 1, end);
    }
}
归并排序

记住归并排序需要额外的空间!!!可以在merge过程中临时申请数组空间,但过多的new可能非常耗时,所以也可以在mergesort一开始就申请数组空间,后面的操作中共用这个空间。

应用场合:mergeSort出现概率不高,因为凡是要sort的地方一般都是quicksort了,但是它主要以两种形式出现,一是考Merge,两个有序数组的merge,多个有序数组的merge,两个有序链表的merge,多个有序链表的merge。二是用mergeSort求逆序数。考查的重点一般不在于sort而在于merge,要是真要用mergeSort的话,印象中就是单向链表的排序。 代码:

void merge(int*arr, int p, int q, int r)
{
    int n1 = q - p + 1;
    int n2 = r - q;
    int* L = new int[n1];
    int* R = new int[n2];
    for(int i = 0; i < n1; i++)
        L[i] = arr[p + i];
    for(int j = 0; j < n2; j++)
        R[j] = arr[q + j + 1];
    int i = j = 0; //新数组中左右两部分索引
    int k = p;     //原数组中索引
    while(i < n1 && j < n2)
    {
        if(L[i] <= R[j])
            arr[k++] = L[i++];
        else
            arr[k++] = R[j++];
    }
    for(; i < n1; i++)
        arr[k++] = L[i];
    for(; j < n2; j++)
        arr[k++] = R[j];
}

void mergesort(int* arr, int p, int r)
{
    int q;
    if(p < r)
    {
        q = (p + r) / 2;
        mergesort(arr, p, q);
        mergesort(arr, q + 1, r);
        merge(arr, p, q, r);
    }
}
关于堆排序

以最小堆为例: 堆的存储:一般都用数组来表示堆,i结点的父结点下标就为(i-1)/2。它的左右子结点下标分别为2 * i + 1和2 * i + 2。

堆的插入:每次插入都是将新数据放在数组最后,也就是堆的底层最右位置。可以发现从这个新数据的父结点到根结点必然为一个有序的数列,现在的任务是将这个新数据插入到这个有序数据中--这就类似于直接插入排序中将一个数据并入到有序区间中。

堆的删除:删除总是发生在根A[0]处,然后用最后一个元素来填补空缺位置,接着再从根结点开始进行一次从上向下的调整。调整时先在左右儿子结点中找最小的,如果父结点比这个最小的子结点还小说明不需要调整了,反之将父结点和它交换后再考虑后面的结点。相当于从根结点将一个数据的"下沉"过程。

堆化数组:对叶子结点来说,可以认为它已经是一个合法的堆了,所以只需从下往上的第一个最右非叶子结点开始向下调整。

堆排序: 堆建好之后堆中第0个数据是堆中最小的数据。取出这个数据再执行堆的删除操作。这样堆中第0个数据又是堆中最小的数据,重复上述步骤直至堆中只有一个数据时就直接取出这个数据。由于堆也是用数组模拟的,故堆化数组后,第一次将A[0]与A[n - 1]交换,再对A[0...n-2]重新恢复堆。第二次将A[0]与A[n-2]交换,再对A[0...n - 3]重新恢复堆,重复这样的操作直到A[0]与A[1]交换。由于每次都是将最小的数据并入到后面的有序区间,故操作完成后整个数组就有序了。有点类似于直接选择排序。

tip:最小堆排序后是递减数组,要得到递增数组,可以使用最大堆。

////堆排序////
//从i结点开始调整,n为结点总数。i结点的孩子结点为2*i+1, 2*i+2
void MinheapFixdown(int a[], int i, int n)
{
    int j = 2 * i + 1;
    int tmp = a[i];
    while(j < n)
    {
        //在左右孩子中找最小的,并更新j为最小孩子的索引
        if(j + 1 < n && a[j + 1] < a[j]) 
            j++;
        if(a[j] >= tmp)
            break;
        a[i] = a[j];
        i = j;
        j = 2 * i + 1;
    }
    a[i] = tmp;
}

//建立最小堆
void MakeMinheap(int a[], int n)
{
    for(int i = n &#47; 2 - 1; i >= 0; --i)
        MinheapFixdown(a, i, n);
}

//堆排序
void MinheapSort(int a[], int n)
{
    for(int i = n - 1; i >= 1; --i)
    {
        swap(a[i], a[0]);
        MinheapFixdown(a, 0, i);
    }
}

////堆删除////
//删除元素。只删除根a[0]元素,用底层最右结点填补
void MinheapDeleteNumber(int a[], int n)
{
    swap(a[0], a[n - 1]);
    MinheapFixdown(a, 0, n - 1);
}

////堆的插入////
//新加入i结点,其父节点为(i - 1)
void MinheapFixup(int a[], int i)
{
    int j = (i - 1) / 2;
    int tmp = a[i];
    while(j >= 0 && i != 0)
    {
        if(a[j] <= tmp)
            break;
        a[i] = a[j];
        i = j;
        j = (i - 1) / 2;
    }
    a[i] = tmp;
}

//结点插入位置为底层最右位置
void MinheapInsertNumber(int a[], int n, int x)
{
    a[n] = x;//添加一个新元素
    MinheapFixup(a, n);
}

Leetcode上相关的题目

1.Merge k Sorted Lists

2.Merge Two Sorted Lists

3.Merge Sorted Array

crystal /
Published under (CC) BY-NC-SA in categories 面试  tagged with 算法  leetcode