对排序算法的初步探究

初学排序算法,我觉得只需要掌握算法的精髓,没必要把所有算法都实现一遍,下面我会实现一些经典的排序算法。(均采用C++实现)

学习的排序算法包含:

1》插入排序(直接插入排序、希尔排序)

2》选择排序(简单选择排序、堆排序)

3》交换排序(快速排序、冒泡排序)

4》归并排序

5》基数排序

我认为初学者掌握基本的排序算法的思想即可,其他排序算法基于特定的数据结构和存储结构,遇到具体的实例再学习即可。

下面就开始学习了。

插入排序

插入排序:把一个数插入到一个有序的序列中,并要求插入后此数据序列仍然有序。这种排序思想就是插入排序。

直接插入排序:把余下元素一个一个插入有序表中,每次都从有序表的最后一个元素开始比较,若是小于该元素:data<a[i],则再与前一个元素比较data[i-1],...直到找到合适位置,最后把该位置及其以后的元素都向后移动:a[j]=a[j-1],腾出一个位置让新元素插入。这也是最简单的插入排序算法。

下面是直接插入排序的简单实现:

void insert_sort(int *a,int n)
{
    if(a == NULL || n <= 1)
    return;
    for(int i=1;i<n;i++)
    {
        /* 存放当前无序的初始序号,
        *初始先将数组的第一个元素构成有序序列,
        *将数组的后面的序列看成无序序列,
        *从数组的第二个元素开始做插入排序
        *注意:有序序列已经是从大到小排好的
        */
        int j=i;
        int temp=a[i];
        while(j && temp<a[j-1]) //一直找到在有序序列中的位置,记录在j中
        {
            a[j]=a[j-1]; //不断将数组元素往后移动,腾出位置
            j--;
        }
        a[j]=temp; //插入temp,即存放的无序序列中的第一个值
    }

}

交换插入排序:可以在比较的过程中,直接交换前后位置的元素,一步一步地把插入的元素交换到合适的位置。

void insert_sort(int *a,int n)
{
    if(a == NULL || n <= 1)
    return;
    for(int i=1;i<n;i++)
    {

        int j=i;
        //一边插入一边比较交换排序,不需要元素后移腾出空间。
        while(j && temp<a[j-1])
        {
            int temp=a[i];
            a[j] = a[j-1];
            a[j-1] = temp;
            j--;
        }
    }

}

折半插入排序:在直接插入中,我们每次都是把一个元素插入到一个有序的序列中。为了使找到位置更高效,我们可以借鉴二分查找的方法,减少比较次数,快速找到合适的插入位置。这就是折半插入的来历。

void binary_insert_sort(int *a,int n)
{
    if(a == NULL || n <= 1) return;
    int low,high,mid;
    for(int i=1;i<n;i++)
    {
        low = 0;
        high = i-1;
        while(low <= high) //采用左闭右闭的区间
        {
            mid = low + (high - low)>>1; //不写成/2是为了防止溢出
            if(a[i]>a[mid])   //不断的判断条件,来改变low,mid,high
                low = mid + 1;
            else
                high = mid - 1;

        }  //循环结束的low值就是这个元素插入的位置,至于为什么,自己画个程序的流程就明白了。值就是这个元素插入的位置,至于为什么,自己画个程序的流程就明白了。

        int temp = a[i];
        //不断的将元素后移
        for(int j=i;j>low;j--)
            a[j] = a[j-1];
        a[low] = temp;
    }

}

希尔排序

我们知道当一个序列基本有序时,直接插入会变得很高效。因为此时只需少量的移动元素,操作集中在元素的比较上。基于这种想法,我们就试图把一个序列在进行直接插入前调整得尽量有序。这就是希尔排序(Shell Sort)的核心思路。(Shell只是算法发明者的名字,无特殊含义)

那到底该怎么做呢?

希尔排序一反以前的做法,插入为何只在相邻的元素之间进行,不相邻的同样可以进行。于是,希尔排序也被形象地称为”跳着插“。

那应该隔几个元素进行插入呢?

这就说到希尔排序的另一个名字:缩小增量排序(Diminishing Increment Sort)。实际上这个增量序列或者说步长序列是可以提前指定的。不同的序列可以极大地影响到算法效率,至于最好的步长序列貌似还在研究。不过可以肯定的是最后的一个步长一定是1。因为最后一次是直接插入。

先来看一种最简单、也最常用的步长序列:n/2、n/2/2、... 1 (n是待排序的元素个数)。也是就说,初始步长是n/2,以后每次减半,直到步长为1。

利用这种步长序列,举一个例子:开始序列中加下划线的字体表示每一趟待排序的数字。

原序列: 21  12  4   9   9   5   78  1    (n=8)

下标:   0   1   2   3   4   5   6   7

第一趟:步长step=4,0、4号元素直接插入排序

开始    21  12  4   9   9   5   78  1

结束    9   12  4   9   21  5   78  1

第二趟:步长step=2, 0、2、4、6号元素直接插入排序

开始    9   12  4   9   21  5   78  1

结束    4   12  9   9   21  5   78  1

第三趟:步长step=1,0、1、2、3、4、5、6、7、8号元素直接插入排序(显然这是整体直接插入排序)

开始    4   12  9  9   21  5   78  1

结束    1    4   5  9   9   12  21  78

如何理解每一趟排序:

  1. 步长序列的长度决定了排序的趟数。
  2. 每一趟排序,并不是所有元素都参与。参与排序的只能是下标为 0、step、2*step...的元素。
  3. 插入排序时,采用何种策略。如直接插入、交换插入、折半插入、表插入或者表折半插入,这可任意选择。
  4. 最后一趟排序的步长一定是1。由第二点可知,最后一趟,全体元素都参与排序。

排序结果中红色9出现在了黑色9的前面,表明希尔排序是不稳定的。

希尔排序的简单实现:

void ShellSort(int *a,int n)
{
    if(a == NULL || n <= 1)
    return;
    for(int step = n/2;step >= 1;step = step/2) //规定步长
    {
        //下面采用交换插入排序算法
        for(int i=step;i<n;i+=step)
            for(int j=i;j>0;j-=step)
            if(a[j]<a[j-step])
                swap(a[j],a[j-step]);
    }
}

插入排序算法的个人感受:整体来说,直接插入排序的算法时间复杂度最坏情况下为O(n^2),空间复杂度可以忽略不计。而采用改进后的插入排序算法(包括希尔排序)只是减少元素移动的个数,提高排序的效率。

选择排序(select sort)

简单选择排序

经过一趟排序,可以从n-i+1(i=1,2...)个记录中选取关键字最小的记录作为有序序列中第i个记录。也就是说,每一趟排序,都会排好一个元素的最终位置。

最简单的是简单选择排序。

简单选择排序(Simple Selection Sort,也叫直接选择排序)

简单选择排序的思想:在每一趟排序中,通过n-i次关键字的比较,从n-i+1个记录中选出关键字最小的记录,并和第i个记录交换,以此确定第i个记录的最终位置。简单说,逐个找出第i小的记录,并将其放到数组的第i个位置。

下面是简单交换排序的算法实现:

void SimpleSelectSort(int *a,int n)
{
    if(a == NULL && n <= 1)
        return;
    int index; //用来记录关键字最小的数组元素的下标
    for(int i=0;i<n;i++)
    {
        index = i;
        for(int j=i+1;j<n;j++) //遍历数组的后面的元素找到关键字最小的元素的下标
        {
            if(a[j]<a[index])
                index = j;
        }
        if(i != index) //当最小关键字的元素的下标和当前的元素不同的时候进行交换
        {
            int temp = a[i];
            a[i] = a[j];
            a[j] = temp;
        }
    }
}

堆排序

堆排序(Heap Sort):使用堆这种数据结构来实现排序。

先看下堆的定义:

最小堆(Min-Heap)是关键码序列{k0,k1,…,kn-1},它具有如下特性:

ki<=k2i+1,

ki<=k2i+2(i=0,1,…)

简单讲:孩子的关键码值大于双亲的。

同理可得,最大堆(Max-Heap)的定义:

ki>=k2i+1,

ki>=k2i+2(i=0,1,…)

同样的:对于最大堆,双亲的关键码值大于两个孩子的(如果有孩子)。

堆的特点:

  1. 堆是一种树形结构,而且是一种特殊的完全二叉树。
  2. 其特殊性表现在:它是局部有序的,其有序性只体现在双亲节点和孩子节点的关系上(树的每一层的大小关系也是可以体现的)。兄弟节点无必然联系。
  3. 最小堆也被称为小顶堆(根节点是最小的),最大堆也被称为大顶堆(根节点是最大的)。我们常利用最小堆实现从小到大的排序,最大堆实现从大到小的排序。

最小堆和最大堆的两个示例图:

这里只讲“堆”这种数据结构的应用之一-------堆排序,而不展开讲“堆”这种数据结构的内部实现的细节。

堆排序的基本原理:大体来说就是根据数组元素建立一个最小堆(从小到大排序)或者最大堆(从大到小排序),然后不断的返回堆顶的元素,删除堆顶的元素后堆内部的结构会发生一些改变,改变结构是为了满足堆的性质,从而不断输出堆顶的元素就是排好序的序列。

堆排序步骤

  1. 先建好堆。
  2. 不断地删除堆顶即可(删除前记得打印堆顶元素),直到只剩下一个元素。

似乎堆的插入操作没有用到。其实,当有新的元素加入到一个已建好堆序的序列中,就用到了。

堆排序的简单实现如下所示:

void HeapSort(int *a,int n)
{
    if(a == NULL && n <= 1)
        return;
    createHeap(a,n); //创建堆
    while(n > 1) //不断删除堆顶的元素,删除前打印。即是堆排序的顺序。
    {
        cout<<a[0];
        deleteAt(a,n,0);
    }
    cout<<a[0];
}

具体的原理以及实现的细节可以参考:http://blog.csdn.net/zhangxiangdavaid/article/details/30069623

交换排序两两比较待排序记录的关键码,若是逆序,则交换,直到无逆序。

其中最简单的交换排序是:冒泡排序。冒泡排序法很稳定,但是不够高效,时间复杂度是O(n^2)。

效率比较高的交换排序法有快速排序。

冒泡排序(Bubble Sort,也叫起泡排序):

不断地比较相邻的记录,若是不满足排序要求,则交换。

交换时,可从前向后,也可从后向前。

看一个从前向后的排序过程:

原序列 12  3   45  33  6

下标   0   1   2   3   4

第一趟:

3   12  45  33  6  (3,12交换)

3   12  45  33  6  (12,45不用交换)

3   12  33  45  6  (45,33交换)

3   12  33  6   45 (45,6交换)

第二趟:

3   12  33  6  45  (3,12不用交换)

3   12  33  6  45  (12,33不用交换)

3   12  6   33 45  (33,6交换)

第三趟:

3   12  6   33 45  (3,12不用交换)

3   6   12  33 45  (12,6交换)

第四趟:

3   6   12  33 45  (3,6不用交换)

结束。

冒泡排序法的实现(未经优化):

void swap(int &a,int& b) //交换函数
{
    int temp = a;
    a = b;
    b = temp;
}

void swap(int& a,int& b) //交换函数也可以这样写,可以自己验证下
{
    if(a!=b)
    {
        a^=b;
        b^=a;
        a^=b;
    }
}

/*冒泡排序法
*从左到右:遍历的数组下标从0--n-1、0--n-2、...、0--1
*可以看作是将最大元素冒泡到最右边
*从右到左:遍历的数组下标从n-1--0、n-1--1、...、n-1--n-2
*可以看作是将最大元素冒泡到最左边
*/
void BubbleSort1(int *a,int n) //冒泡排序法,从左往右
{
    for(int i=1;i<n;i++)
        for(int j=0;j<n-i;j++)
        {
            if(a[j]>a[j+1])
                swap(a[j],a[j+1]);
        }
}

void BubbleSort2(int *a,int n)  //冒泡排序法,从右往左
{
    for(int i=1;i<n;i++)
        for(int j=n-1;j>=i;j--)
        {
            if(a[j]>a[j-1])
                swap(a[j],a[j-1]);
        }
}

现在可以进行一下优化:

如果在某趟排序的时候没有进行数据的交换,那么就表明已经是有序排列的了,此时就不需要进行排序了,结束程序就可以了。

改进的冒泡排序如下:

void swap(int& a,int& b)
{
    a ^= b;
    b ^= a;
    a ^= b;
}

void BuubbleSort(int* a,int n)
{
    if(a == NULL && n >= 1)
        return;
    bool flag = true;  //设置一个标志位,当标志位为true才会循环
    while(flag == true)
    {
        for(int i=1;i<n;i++)
            for(int j=0;j<n-i;j++)
            {
                if(a[j]>a[j+1])
                {
                    swap(a[j],a[j+1]);
                }
                else
                    flag = false; //当某一趟循环中没有数据交换的时候,就会将标志位置false
            }
    }
}

再度优化:下一趟排序向右(或向左)的最远位置,只是简单的减一吗?可否更高效?

可以更加高效,记录上一趟排序时交换元素的最远距离,下一趟排序最远只到这个位置即可。

下面是改进后:

void swap(int& a,int& b)
{
    a ^= b;
    b ^= a;
    a ^= b;
}

void BuubbleSort(int* a,int n)
{
    if(a == NULL && n >= 1)
        return;
    bool flag = true; //仍然设置标志位
    int j = n-1; //初始的循环上限
    while(flag)
    {
        for(int i=0;i<j;i++)
        {
            if(a[i]>a[i+1])
            {
                swap(a[i],a[i+1]);
                j = i; //每次记录交换的下标 ,最后一次的j值就是最远交换的下标,也就是下次的循环上限
            }
            else
                flag = false; //如果某一趟遍历中没有数据的交换,那么就表明已经是排好序的了,可以结束程序了
        }
    }
}

快速排序(Quick Sort)也是一种交换排序,它在排序中采取了分治策略。

快速排序的主要思想

  1. 从待排序列中选取一元素作为轴值(也叫主元)。
  2. 将序列中的剩余元素以该轴值为基准,分为左右两部分。左部分元素不大于轴值,右部分元素不小于轴值。轴值最终位于两部分的分割处。
  3. 对左右两部分重复进行这样的分割,直至无可分割。

从快速排序的算法思想可以看出,这是一递归的过程

要想彻底弄懂快速排序,得解决两个问题:

  1. 如何选择轴值?(轴值不同,对排序有影响吗?)
  2. 如何分割?

问题一:轴值的选取?

轴值的重要性在于:经过分割应使序列尽量分为长度相等的两个部分,这样分治法才会起作用。若是轴值正好为序列的最值,分割后,元素统统跑到一边儿去了,分治法就无效了。算法效率无法提高。-看别人写快排的时候,注意他轴值的选取哦。

问题二:如何分割?

这涉及到具体的技巧和策略。

未完待更新。。。

原文地址:https://www.cnblogs.com/jeavenwong/p/8214152.html

时间: 2024-11-13 15:14:03

对排序算法的初步探究的相关文章

【转】对排序算法的深入探究

OI中有很多排序算法,冒泡排序,插入排序,快速排序,基数排序,以及神一般的bogo排序等等等等. 在这门不断追求速度的学科里,最快的排序方法是什么一直是信息学皇冠上的明珠.基数排序是线性的,但是却有非常大的局限性,仅仅基于比较的排序复杂度的下限是多少呢? 有一天,一个人出来妖言惑众:“基于比较的排序复杂度下限是O(n log n)的.”并且提出了伪证. 他用一棵二叉树来建立模型,通过对树的深度进行探究,从而推出排序的复杂度下限.于是人类停滞不前,再也没有人试图创新. 然而事实上,他的行为有着一个

【数据结构】非比较排序算法(实现计数排序和基数排序)

● 计数排序 1.算法思想: 计数排序是直接定址法的变形.通过开辟一定大小的空间,统计相同数据出现的次数,然后回写到原序列中. 2.步骤: 1)找到序列中的最大和最小数据,确定开辟的空间大小. 2)开辟空间,利用开辟的空间存放各数据的个数. 3)将排好序的序列回写到原序列中. 具体实现如下: void CountSort(int *arr, int size) {  assert(arr);  int min = arr[0];  int max = arr[0];  int num = 0;

排序算法—冒泡排序

*/--> 排序算法-冒泡排序 Table of Contents 1 问题描述 2 冒泡排序(Bubble) 2.1 冒泡排序(一) 2.2 冒泡排序(二) 2.3 冒泡排序(三) 2.4 冒泡排序(四) 3 阅读参考 1 问题描述 引子 排序是数据结构中十分重要的一章,排序算法有很多种,一直没时间整理而且很多排序算法理解的也不是很透彻.希望通过这次整理吃透吧! 排序算法十分多,故分篇进行整理. 说明 本文重点是理解排序算法,而不是完整的程序,所以每节都只有具体排序算法的接口.没有完整的源代码

排序算法&lt;No.2&gt;【桶排序】

算法,是永恒的技能,今天继续算法篇,将研究桶排序. 算法思想: 桶排序,其思想非常简单易懂,就是是将一个数据表分割成许多小数据集,每个数据集对应于一个新的集合(也就是所谓的桶bucket),然后每个bucket各自排序,或用不同的排序算法,或者递归的使用bucket sort算法,往往采用快速排序.是一个典型的divide-and-conquer分而治之的策略. 其中核心思想在于如何将原始待排序的数据划分到不同的桶中,也就是数据映射过程f(x)的定义,这个f(x)关乎桶数据的平衡性(各个桶内的数

第六章 常见排序算法

上章回顾 二叉树的定义 树深度的定义 什么样的二叉树是满二叉树 中序遍历的规则 [email protected]:Kevin-Dfg/[email protected]:Kevin-Dfg/Data-Structures-and-Algorithm-Analysis-in-C.git 第六章 第六章 常见排序算法 常见排序算法 [email protected]:Kevin-Dfg/[email protected]:Kevin-Dfg/Data-Structures-and-Algorith

GDI+学习笔记(九)带插件的排序算法演示器(MFC中的GDI+实例)

带插件的排序算法演示器 本节将通过一个实例来说明GDI+在MFC中的应用.这个算法演示器其实是本人算法系列的一个开端,由于csdn没有树状的目录结构,咱也只好使用链表了不是?好了,废话不多说,开始今天的文章. (一)功能说明 我们初步制定功能如下: (1). 能够通过柱状图,自动展示排序算法的交换比较过程 (2). 能够使用插件的形式进行开发.即,当新完成一个算法后,只需要完成一个插件文件(我们这里使用动态库dll),由主程序加载插件,即可进行执行,而不再需要重新编译主程序. (3). 保证主程

Java实现常见的排序算法

排序的基本概念与分类 排序是我们生活中经常会面对的问题.同学们做操时会按照从矮到高排列;老师查看上课出勤情况时,会按学生学号顺序点名;高考录取时,会按成绩总分降序依次录取等.那排序的严格定义是什么呢? 假设含有 n 个记录的序列为{r1,r2,......,rn}, 其相应的关键字分别为{k1,k2,......,kn} ,需确定 1, 2--, n 的一种排列 p1,p2,.......,使其相应的关键字满足 kp1<=kp2<=-- <=kpn (非递减或非递增) 关系,即使得序列成

内部排序算法比较

一.题目描述 通过随机数据比较各排序算法的关键字比较次数和关键字移动次数,以 及执行时间,取得直观感受. 二.设计要求 一.需求分析 实现各排序算法,分别进行以下各组比较,并进行总结. 一.各算法在不同规模下的比较. 1)比较范围:直接插入排序.冒泡法排序.简单选择排序.快速排序1(自己实现).快速排序2(调用STL).归并排序. 2)比较指标:a)关键字操作次数(比较次数和移动次数之和),b)排序时间.每个指标采用多次重复取平均数记录,重复次数不小于100.注:1次关键字对换按3次移动计算.

常见比较排序算法的耗时测试

一直知道插入排序在输入规模比较小时会有比较好的效率,但这个输入规模多少才算少却无从知晓,今天特意写了几个小程序分别测试了几种排序算法随输入规模增长的耗时情况. 测试环境 CPU 3.0GHz 双核  1G内存   centos虚拟机 g++ 4.9.1 预先构造100W个随机生成的整数数组,计算使用各种排序算法时的总耗时 插入排序  vs  冒泡排序 不出所料,插入排序基本在任何输入规模均优于冒泡排序. 插入排序  vs 快速排序 vs 归并排序 由下图可以看出,在输入规模小于100时,插入排序