排序算法总结与几道排序题

1.快排

快排的思想就是"分而治之".找到一个分区点,然后将小的放在前面,大的放在后面,分区点放在中间.最后发现待排序的区间变为1时停止.

在这里插入图片描述

/*在这个函数中会改变原数组,见下图:*/
/*从头到尾迭代,和支点比较,大的不管,小的换。换了才往后看。最后支点戳中间,算是分界线*/
int partition(vector<int> &a, int l, int r)
{
    int k = l, pivot = a[r];
    int temp = 0;
    for (int i = l; i < r; i++)
    {
        if (a[i] <= pivot) /*相等的元素要么放右边,要么放左边,这里也可以放在右边*/
        {
            std::swap(a[i], a[k++]);
        }
    }
    std::swap(a[k], a[r]);
    return k;
}
/*快速排序递归函数*/
void quick_sort(vector<int> &a, int l, int r)
{
    if (l >= r)
        return;
    int q = partition(a, l, r); /*获取分区点*/
    quick_sort(a, l, q - 1);
    quick_sort(a, q + 1, r);
}

void QuickSortWithRecursive(vector<int> &a)
{
    quick_sort(a, 0, a.size() - 1);
}

其中经过partition函数分区之后,数组就会变成这样.partition 在一般情况下会选择以数组的尾巴为分界线

在这里插入图片描述

  • 时间复杂度:大多数情况下时间复杂度O(nlog(n)).极少数情况下,效率是O(n^2).(所谓的优化其实就是将这个概率不断降低)
  • 空间复杂度O(1).原地排序
  • 是否稳定:不稳定

2. 堆排序(完全二叉树形数据结构)

只要满足这两点,它就是一个堆。

  • 堆是一个完全二叉树; (最后一层靠左排列)
  • 堆中每一个节点的值都必须大于等于(或小于等于)其子树中每个节点的值。

对于同一组数据,我们可以构建多种不同形态的堆。

如何实现一个堆?

完全二叉树适合用数组来存储数据,这样会比较节省内存,因为节点会一个接一个出现在数组中,而不会产生空白.

在这里插入图片描述
从图中我们可以看到,数组下标从1开始,对于数组中下标为 i 的节点:

  • 左子节点,就是下标为 i∗2 的节点,
  • 右子节点,就是下标为 i∗2+1 的节点
  • 父节点,就是下标为 i/2 的节点。

插入元素

堆化有两种方式:从上往下和从下往上

从下往上

在这里插入图片描述

删除堆顶元素

假设我们构造的是大顶堆,堆顶元素就是最大的元素。当我们删除堆顶元素之后,就需要把第二大的元素放到堆顶,那第二大元素肯定会出现在左右子节点中。然后我们再迭代地删除第二大节点,以此类推,直到叶子节点被删除。

这里我也画了一个分解图。不过这种方法有点问题,就是最后堆化出来的堆并不满足完全二叉树的特性。

在这里插入图片描述

从上往下(其实就是把节点从上面往下堆化)

实际上,我们稍微改变一下思路,就可以解决这个问题。你看我画的下面这幅图。我们把最后一个节点放到堆顶,然后利用同样的父子节点对比方法。对于不满足父子节点大小关系的,互换两个节点,并且重复进行这个过程,直到父子节点之间满足大小关系为止。这就是从上往下的堆化方法

因为我们移除的是数组中的最后一个元素,而在堆化的过程中,都是交换操作,不会出现数组中的“空洞”,所以这种方法堆化之后的结果,肯定满足完全二叉树的特性。

在这里插入图片描述

一个包含 n个节点的完全二叉树,树的高度不会超过 log2n。堆化的过程是顺着节点所在路径比较交换的,所以堆化的时间复杂度跟树的高度成正比,也就是 O(logn)。插入数据和删除堆顶元素的主要逻辑就是堆化,所以,往堆中插入一个元素和删除堆顶元素的时间复杂度都是 O(logn)

堆排序时间复杂度:O(nlogn),原地算法

如何基于堆实现排序?(两步:建堆与排序)

1. 建堆
  • 第一种方法就是:从第二个元素开始,依次插入
  • 第二种方法是:从后往前处理数组,并且每个数据都是从上往下堆化

由于第二种方法比较有效率,所以直接上第二种方法即可

因为叶子节点往下堆化只能自己跟自己比较,所以我们直接从第一个非叶子节点开始,依次堆化就行了。

对于完全二叉树而言:

  • 叶子节点是:n/2+1到n
  • 非叶子节点是:1到n/2
void heapify(vector<int> &a, int n, int i)
{
    while (true)
    {
        int maxPos = i;
        if (i * 2 <= n && a[i] < a[i * 2])
            maxPos = i * 2;
        if (i * 2 + 1 <= n && a[maxPos] < a[i * 2 + 1])
            maxPos = i * 2 + 1;
        if (maxPos == i)
            break;
        swap(a[i], a[maxPos]);
        i = maxPos;
    }
}

void buildHeap(vector<int> &a, int n)
{
    for (int i = n / 2; i >= 1; --i)
    {
        heapify(a, n, i);

        for (auto i : a)
            cout << i << "  ";
        cout << endl;
        sleep(1);
    }
    for (auto i : a)
        cout << i << "  ";
    cout << endl;
    sleep(1);
}

建堆的时间复杂度:O(n)

2.排序(O(n))

建堆结束之后,数组中的数据已经是按照大顶堆的特性来组织的。数组中的第一个元素就是堆顶,也就是最大的元素。我们把它跟最后一个元素交换,那最大元素就放到了下标为 n 的位置。然后堆该n-1个元素再次堆化就行了
在这里插入图片描述

// n 表示数据的个数,数组 a 中的数据从下标 1 到 n 的位置。
void sort(vector<int> &a, int n)
{
    buildHeap(a, n);
    int k = n;
    while (k > 1)
    {
        swap(a[1], a[k]);
        --k;
        heapify(a, k, 1);
    }
}

堆排序不是稳定的排序算法,因为在排序的过程,存在将堆的最后一个节点跟堆顶节点互换的操作,所以就有可能改变值相同数据的原始相对顺序。

提问:为什么它与快排的时间复杂度相同,但是快排就是比它要性能好呐?

  • 第一点,堆排序数据访问的方式没有快速排序友好。(没有顺序遍历,对CPU缓存不友好)
  • 第二点,对于同样的数据,在排序过程中,堆排序算法的数据交换次数要多于快速排序。(因为需要建堆,就会打乱有些有序序列)

3.冒泡与选择

冒泡:冒泡排序只会操作相邻的两个数据。每次冒泡操作都会对相邻的两个数据进行比较,如果前者比后者大,就互相交换,最后就会找到一个最大的落在数组最后.重复以上工作n次即可完成排序.

void BubbleSort(vector<int> a)
{
    int len = a.size();
    if (len <= 1)
        return;
    for (int i = 0; i < len; i++)
    {
        for (int j = 0; j < len - i - 1; j++)
        {
            if (a[j + 1] < a[j])
                std::swap(a[j], a[j + 1]);
        }
    }
}
  • 时间复杂度:平均时间复杂度O(n^2).
  • 空间复杂度O(1).原地排序
  • 是否稳定:稳定

选择:每次找到最小的一个元素放到已经排好序的数组的尾部即可.

void SelectSort(vector<int> a)
{
    int len = a.size();
    if (len <= 1)
        return;
    for (int i = 0; i < len; i++)
    {
        for (int j = i + 1; j < len; j++)
        {
            if (a[j] < a[i])
                std::swap(a[j], a[i]);
        }
    }
}

2.插入

就是将后面的数据与前面的数据进行相比,如果小的话就将前面的数据向后移动,然后将其插入即可.

void InsertSort(vector<int> &a)
{
    int len = a.size();
    if (len <= 1)
        return;
    for (int i = 1; i < len; i++)
    {
        int key = a[i];
        int j = i - 1;
        /*查找插入的位置*/
        for (; j >= 0; j--)
        {
            if (a[j] > key)
                a[j + 1] = a[j]; /*数据移动*/
            else
                break;
        }
        a[j + 1] = key; //数据插入
    }
}
  • 时间复杂度:平均时间复杂度O(n^2).因为我们在一个数组中插入数据是O(n),最坏也是n的平方
  • 空间复杂度O(1).原地排序
  • 是否稳定:稳定

4. 拓扑排序(与图有关,图用(逆)邻接表存储)

我们先来看一个生活中的拓扑排序的例子。
   我们在穿衣服的时候都有一定的顺序,我们可以把这种顺序想成,衣服与衣服之间有一定的依赖关系。比如说,你必须先穿袜子才能穿鞋,先穿内裤才能穿秋裤。假设我们现在有八件衣服要穿,它们之间的两两依赖关系我们已经很清楚了,那如何安排一个穿衣序列,能够满足所有的两两之间的依赖关系?
   这就是个拓扑排序问题。从这个例子中,你应该能想到,在很多时候,拓扑排序的序列并不是唯一的。你可以看我画的这幅图,我找到了好几种满足这些局部先后关系的穿衣序列。
在这里插入图片描述
拓扑排序的一个经典问题就是"如何确定代码源文件的编译依赖关系?"什么是依赖关系,这个就不属于本文讨论范畴了.那么该如何解决呐?

   我们可以把源文件与源文件之间的依赖关系,抽象成一个有向图。每个源文件对应图中的一个顶点,源文件之间的依赖关系就是顶点之间的边。 如果 a 先于 b 执行,也就是说 b 依赖于 a,那么就在顶点 a 和顶点 b 之间,构建一条从 a 指向 b 的边。而且,这个图不仅要是有向图,还要是一个有向无环图,也就是不能存在像 a->b->c->a 这样的循环依赖关系。因为图中一旦出现环,拓扑排序就无法工作了。实际上,拓扑排序本身就是基于有向无环图的一个算法。


OK ,现在我们有思路了,但是我们如何实现拓扑排序呐?这里有两种方法:

(1)Kahn 算法

Kahn 算法实际上用的是贪心算法思想,思路非常简单、好懂。
   定义数据结构的时候,如果 s 需要先于 t 执行,那就添加一条 s 指向 t 的边。所以,如果某个顶点入度为 0, 也就表示,没有任何顶点必须先于这个顶点执行,那么这个顶点就可以执行了。
   我们先从图中,找出一个入度为 0 的顶点,将其输出到拓扑排序的结果序列中(对应代码中就是把它打印出来),并且把这个顶点从图中删除(也就是把这个顶点<指向的顶点的入度>都减 1)。我们循环执行上面的过程,直到所有的顶点都被输出。最后输出的序列,就是满足局部依赖关系的拓扑排序。

(2)DFS 算法

首先,我们去思考DFS算法的思想,递归,从开始到结束,再从结束到上一步.那么我们邻接表是否适合?答案不适合,应该用逆邻接表.具体看代码解释:

#include <iostream>
#include <vector>
#include <list>
#include <algorithm>
#include <queue>

// 如果点的表示还需要插入等操作的话,就将vector 改为 std::list
using namespace std;

class Graph
{
  public:
    Graph(int v)
    {
        this->v = v;
        for (int i = 0; i < v; i++)
            adj.push_back(std::list<int>());
    }
    void AddEdge(int s, int t)//s->t
    {
        adj[s].push_back(t);
    }
    void TopSortByKahn()
    {
        vector<int> inDegree(v, 0); //统计每个顶点的入度
        for (int i = 0; i < v; i++)
        {
            for (auto t : adj[i])
            {
                inDegree[t]++;
            }
        }
        queue<int> que;
        for (int i = 0; i < v; i++)
        {
            if (inDegree[i] == 0)
                que.push(i);
        }
        while (!que.empty())
        {
            int head = que.front();
            que.pop();
            cout << "->" << head;
            for (auto t : adj[head])
            {
                inDegree[t]--;
                if (inDegree[t] == 0)
                    que.push(t);
            }
        }
    }
    void TopSortByDFS()
    {
        // 先构建逆邻接表
        std::vector<std::list<int>> inverseadj(v, std::list<int>());
        for (int i = 0; i < v; i++) // 通过邻接表生成逆邻接表
        {
            for (auto t : adj[i])
            {
                inverseadj[t].push_back(i);
            }
        }
        bool visted[v] = {false};
        for (int i = 0; i < v; i++) //dfs 
        {
            if (visted[i] == false)
            {
                visted[i] = true;
                dfs(i, inverseadj, visted);
            }
        }
    }

  private:
    void dfs(int vertex, const std::vector<std::list<int>> &inverseadj, 
    bool visted[])
    {
        // cout << vertex << endl;
        for (auto t : inverseadj[vertex])
        {
            if (visted[t] == true)
                continue;
            else
            {
                visted[t] = true;
                dfs(t, inverseadj, visted);
            }
        }// 先把 vertex "这个顶点依赖的所有顶点"都打印出来之后,再打印它自己(核心)
        cout << "->" << vertex;
    }

  private:
    int v;                           //顶点个数
    std::vector<std::list<int>> adj; //邻接表 
};
int main(void)
{
    Graph g(6);
    g.AddEdge(5, 2);
    g.AddEdge(5, 0);
    g.AddEdge(4, 0);
    g.AddEdge(4, 1);
    g.AddEdge(2, 3);
    g.AddEdge(3, 1);

    g.TopSortByKahn();
    cout << endl;
    g.TopSortByDFS();
    cout << endl;
    return 0;
}

所使用的图是:
在这里插入图片描述

5.

6.

7.几道排序题目:

(1)148. 排序链表

在 O(n log n) 时间复杂度和常数级空间复杂度下,对链表进行排序。

示例 1:

输入: 4->2->1->3
输出: 1->2->3->4
示例 2:

输入: -1->5->3->4->0
输出: -1->0->3->4->5

考虑到O(nlogn)的排序算法有归并,快排,堆排.这里直接使用归并排序.

具体思路就是:把链表不断的断成两条,直到剩一个节点,然后再合并起来就行了.具体见代码:
在这里插入图片描述

class Solution
{
  public:
    ListNode *sortList(ListNode *head)
    {
        if (head == NULL || head->next == NULL)
            return head;

        ListNode *mid = findMid(head);

        ListNode *left = sortList(head);
        ListNode *right = sortList(mid);

        return mergeTwoLists(left, right);
    }

    ListNode *findMid(ListNode *head)
    {
        if (!head || !head->next)
            return head;
        ListNode *slow = head;
        ListNode *fast = head->next->next;
        while (fast)
        {
            slow = slow->next;
            fast = fast->next;
            if (fast)
                fast = fast->next;
        }
        ListNode *mid = slow->next;
        slow->next = NULL;
        return mid;
    }
    ListNode *mergeTwoLists(ListNode *l1, ListNode *l2)
    {
        ListNode result = ListNode(-100);
        ListNode *test = &result;

        while (l1 && l2)
        {
            //注意这里判断了相等的情况
            //l1->val == l2->val
            if (l1->val <= l2->val)
            {
                test->next = l1;
                l1 = l1->next;
            }
            else
            {
                test->next = l2;
                l2 = l2->next;
            }
            test = test->next;
        }
        if (l1)
            test->next = l1;
        if (l2)
            test->next = l2;
        return result.next;
    }
};

(2)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值