排序算法

数据结构复习一—–排序算法

这是
这是数据结构中有的相关的排序算法

插入类排序

直接插入类排序

将第i个记录插入已经排好序的i-1个元素中,每次依次比较和移动,i从2-n(第一个视为已经排好序的元素),保留一个key存放array[i]的数据,以防数据大的元素后移使会覆盖掉要插入的数据。

每次只要找到比要插入的数据大的数据就交换两个元素的位置

代码块
void insert(int *array, int size)
{
    for (int i = 1; i < size; i++)
    {
        int key = array[i];//作为保留的array[i],防止在移动中覆盖原有数据
        int j = i - 1;
        while (array[j]>key)
        {
            array[j + 1] = array[j];
            j--;
        }
        if ((j+1)!=i)//如果顺序已经顺序,就不用在排了
        array[j+1] = key;
    }
}


int main()
{
    int array[] = { 48, 62, 35, 77, 55, 14, 35, 98 };
    int size = sizeof(array) / sizeof(array[0]);
    insert(array, size);
    print(array, size);
    system("pause");
}

算法核心

  • 要设计key来保存array[i]中的数据,以防大的数据元素将其覆盖
  • 从后往前插入数据,保证算法的稳定性
  • 元素的插入和移动在同一循环下

时间复杂度 该算法的耗时只在比较和移动元素上

  • 最好的情况,元素已经是顺序了,此时只需进行n-1次比较,而不需搬移元素,此时的时间复杂度为O(N)
  • 最坏的情况,元素按逆序排列,此时比较的此时为1+2+….+n-1=n(n-1)/2,而交换次序大致为2+3+……n=(n-1)(n+2)/2;所以时间复杂度为O(N^2)
  • 所以时间复杂度为O(N^2),空间复杂度为O(1)
  • 算法是稳定的,因为是从后往前插入数据,且while(array[j]>key)保证了相同的元素不会改变相应的位置
  • 使用场景
    n较小,且数据基本有序

折半插入

在依据上述直接插入的例子中,比较和插入是同时进行的,那么,是否可以对其相应拆解,各自做优化呢??我们知道对于有序数组的查找二分查找是相对比较优化的选择方案,其时间复杂度大致为log2n,对于顺序表元素的插入只能依次向后逐渐插入,谈不上优化
所以折半查找的算法思想就是先用二分查找找到相应的插入位置,再进行元素的依次移动
这里写图片描述

代码块
void insert_op(int *array, int size)
{
    for (int i = 1; i < size; i++)
    {
        int left = 0;
        int right = i - 1;//前i-1个已排好序的数据中查找位置
        int key = array[i];//带插入元素
        //查找待插入的位置
        while (left <= right)
        {
            int mid = left + ((right - left) >> 1);
            if (array[mid]>key)
            {
                right = mid - 1;
            }
            else
            {
                left = mid + 1;
            }
        }
        //此时left即为带插入的位置
        //移动元素位置
        if (i != left)
        {
            for (int j = i - 1; j >= left; --j)
            {
                array[j + 1] = array[j];
            }
            array[left] = key;
        }
    }
}

int main()
{
    int array[] = { 48, 62, 35, 77, 55, 14, 35, 98 };
    int size = sizeof(array) / sizeof(array[0]);
    //insert(array, size);
    insert_op(array, size);
    print(array, size);
    system("pause");
}
思考
  • 查找和移动的代码能放在同一个循环中吗??
    不能,若是放在同一个循环中,每次刚查找完一次的位置的left就被传给了移动数据的元素,此时的left还不是二分查找的最终的元素的位置。所以只能查找完成后再移动
  • 时间复杂度
    查找的时间复杂度为nlog2n,而移动元素的时间复杂度通直接查找是相同的为O(n^2)
    • 使用场景
      若是n越大,折半插入比直接插入的最坏情况好很多,但是比最好的还是要差
  • 空间复杂度为O(1)

希尔排序

直接插入排序算法,在待插入的元素基本有序且n较小时,其算法的性能最佳。
- 为什么用希尔排序
而希尔算法依据直接插入的思想,将数据按增量划分多个子序列,对子序列进行直接插入排序。使得当N较大时,也能按照较优的性能进行插入排序。(n较小)
对子序列进行直接插入排序后,使得总的序列基本有序,也能按照较优的性能进行插入排序。(基本有序)
这里写图片描述
上述的增量是按照d=d/2的算法实现的,但是可以看到是存在缺陷的,除最后一次d=1外,其他的情况下偶数位置和奇数位置是无法进行比较的。
所以改进后的d=d/3+1;当然,增量的取值算法是不唯一的,这是较为优化的一种。

代码块
void shell(int *array, int size)
{
    int gap = size;
    while (gap > 1)
    {
        gap = gap / 3 + 1;
        for (int i = gap; i < size; i++)
        {
            int left = i - gap;
            int key = array[i];
            //设置循环的主要目的是为了能够按照增量从后往前一次的对数据进行排序,保证了每条增量线上的元素是有序的
            while (array[left]>key && left >= 0)
            {
                array[left+gap] = array[left];
                left = left - gap;
            }
            array[left + gap] = key;
        }
    }
}

int main()
{
    int array[] = { 48, 62, 35, 77, 55, 14, 35, 98 };
    int size = sizeof(array) / sizeof(array[0]);
    //insert(array, size);
    //insert_op(array, size);
    shell(array, size);
    print(array, size);
    system("pause");
}
  • 希尔排序的时间复杂度是和增量相关的,一般认为它的时间复杂度为O(n^1.5),想算的朋友们可以去算一下,这里就不详细解释了
  • 空间复杂度为O(1)
  • 该算法是不稳定的,2 4 1 2,要说的话,因为是跨增量去交换元素,所以相同数值的元素的书序要依据和它比较的元素数值,所以顺序是不一定的
  • 使用场景
    使用于中等规模的数据排序(n<=1000),具有较好的效率

交换排序
冒泡排序

非常简单的一种排序算法,进行n-1趟的排序,以第i趟为例,若是逆序排列的话,需要进行n-i次的比较,进行3(n-i)次交换。所以经过N-1次的交换排序后,总的比较次数为n-1+……+2+1=n(n-1)/2次的比较,需要进行3n(n-1)/2的交换。

算法思想

每排序依次将最大的元素移至数组的最大下标出,下一次只需比较n-2的位置即可,进行n-1次的排序。
这里写图片描述

代码块
void swap(int *left, int *right)
{
    int tmp = *left;
    *left = *right;
    *right = tmp;
}

void BubbleSort(int *array, int size)
{
    int change = 0;
    for (int i = 0; i < size - 1; i++)
    {
        change = 0;
        for (int j = 0; j < size - 1 - i; j++)
        {
            if (array[j]>array[j + 1])
            {
                swap(&array[j], &array[j + 1]);
                change = 1;
            }
        }
        if (!change)
            return;
    }
}

int main()
{
    int array[] = { 48, 62, 35, 77, 55, 14, 35, 98 };
    int size = sizeof(array) / sizeof(array[0]);
    BubbleSort(array, size);
    print(array, size);
    return 0;
}
  • 时间复杂度
    最好情况:数组已经是有序的,只需进行n-1次比较即可。
    最坏的情况:数组是逆序的,此时需进行1+2+……+n-1=n(n-1)/2次比较,需要3n(n-1)/2次的交换。
    所以时间复杂度为O(n^2);
    空间复杂度为O(1);
  • 算法的稳定性
    因为是从后逐渐缩小排序的范围,且if(array[j]>array[j+1])保证了算法的稳定性。
  • 使用场景
    n较小且有序

快速排序

依据冒泡排序的思想,改善了冒泡排序只可以交换相邻两个元素的缺点,可以交换不相邻的两个元素,从而大大提高了排序的速度。

算法思想

找一个基准值,将大于基准值的排在基准值的后面,将小于基准值的排在基准值的前面。这样就分割成了两个字表,再将连个字表按照上述规则再进行划分,直至字表的长度不超过1。

算法注意点
  • 怎样设计能够使算法最为优化??
    时间的消耗点在于进行多少次排序?
    我们可以将其转化为树的思想,求其递归深度。怎样使深度最小呢?此时莫过于完全二叉树,所以即每次分的子序列最好可以分为一半。空间复杂度为O(log2n),这就需要在找基准值的时候采用合理的算法,在此,用到的是三数取中法,即首尾元素和中间元素做比较,选取中间值。
#代码块
int GetMidDataIndex(int* array, int left, int right)
{
    int mid = left + ((right - left) >> 1);
    if (array[left] < array[right-1])
    {
        if (array[mid]>array[right-1])
            return right-1;
        else if (array[mid] < array[left])
            return left;
        else
            return mid;
    }
    else
    {
        if (array[mid]<array[right-1])
            return right-1;
        else if (array[mid] > array[left])
            return left;
        else
            return mid;
    }
}

而对于相应的一趟的快速排序,在这里详细讲解三种方法。

挖坑法

这里写图片描述

代码块
int QKPass1(int *array, int left, int right)
{
    int pos = GetMidDataIndex(array, left, right);
    int begin = left;
    int end = right-1;
    int key = array[pos];
    swap(&array[pos], &array[begin]);
    while (begin < end)
    {
        while (array[end]>key && begin < end)
        {
            end--;
        }
        //array[pos] = array[end];
        //在这里出现了问题,由于第一次是end和pos的位置交换,导致后面再进行beginend交换时,无法实现
        //作出改进,将pos处的值与begin处的值在最开始未进入循环的时候先进行交换
        if (begin < end)
        {
            array[begin] = array[end];
            begin++;//此处右边悬空,要让左侧走
        }
        while (array[begin] < key&&begin < end)
        {
            begin++;
        }
        if (begin < end)
        {
            array[end] = array[begin];
            end--;//此处左边悬空,要让右侧走
        }
    }
    array[begin] = key;
    return begin;
}
特别注意两点
  • 在最开始的时候要交换基准值的位置和begin的位置,因为在以后的移动过程中始终是begin和end的交换,不涉及pos,所以开始的时候就要交换两者的位置
  • 此外,注意在将end的位置悬空时,要让begin++;在begin的位置悬空时,就要end–;

前后指针法

这里写图片描述
利用两个指针,主要让后面的指针寻找比基准值小的值,若是此时前面的指针指向的值比基准值大,则交换两个元素的值,否则各自往后一找寻合适的交换位置。
最终的prev的位置即为基准值。

代码块
int QKPass2(int *array, int left, int right)
{
    int pcur = left;
    int prev = pcur - 1;
    int pos = GetMidDataIndex(array, left, right);
    int key = array[pos];
    swap(&array[pos], &array[right - 1]);
    while (pcur < right)
    {
        if (array[pcur] < key && (++prev) != pcur)
        {
            if (array[prev]>key)
                swap(&array[prev], &array[pcur]);
            else
                prev++;
        }
        pcur++;
    }
    swap(&array[++prev], &array[right - 1]);
    //特别注意此处,要将prev++
    return prev;
}
有两点需要注意的
  • 在判断是否需要交换的时候,不仅需要判断pcur所指向的值小于key,还要确保交换位置的prev要大于key,此时才能交换。
  • 在最后的key值交换中,要记得prev++

hoare版本

和前后指针法类似,一个从最左边开始找大于key的,一个从最右边开始找小于key的,找到两者交换。直到最后begin==end
这里写图片描述

代码块
int QKPass3(int *array, int left, int right)
{
    int begin = 0;
    int end = right - 2;//不需要和最后一个key值做比较
    int pos = GetMidDataIndex(array, left, right);
    int key = array[pos];
    swap(&array[pos], &array[right - 1]);
    while (begin < end)
    {
        while (array[begin] < key&&begin < end)
        {
            begin++;
        }
        while (array[end]> key&&begin < end)
        {
            end--;
        }
        if (begin < end)
            swap(&array[begin], &array[end]);
        begin++;
        end--;
    }
        swap(&array[begin], &array[right - 1]);
    return begin;
}
排序算法的递归写法
代码
void QK(int *array, int left, int right)
{
    if (left < right)
    {
        int pos = QKPass3(array, left, right);
        QK(array, left, pos);
        QK(array, pos + 1, right);
    }
}
非递归(栈的思想)

快排就是一分为二的思想,所以利用栈的思想也可以完美的解决。先让右数入队列,再让左数入队列,在依次取栈顶元素,依次出栈。这样就可以实现递归了。

代码
快速排序总结
  • 最好的情况,每次分两部分O(nlog2n);最坏的情况,已经排好序了,每次需要进行n-i次比较,总共n(n-1)/2次比较;
    -时间复杂度为O(nlog2n);
  • **空间复杂度为O(log2n);相当于树的深度
  • **不稳定(3,3,2);
选择排序
普通选择排序

每次在无序队列中“选择”出最小值,放到有序队列的最后,并从无序队列中去除该值
选择排序的平均时间复杂度比冒泡排序的稍低
原因:

  • 同样数据的情况下,两种算法的循环次数是一样的,但选择排序只有0到1次交换,而冒泡排序只有0到n次交换。

  • 冒泡排序是每一次都可能要交换,而选择排序是比较时记下a[i]的位置,最后来交换,所以其交换的过程是不一样的,但查找的过程是一样的。

代码
void selectsort(int *array, int size)
{
    for (int i = 0; i < size-1; ++i)//n-1趟
    {
        int maxpos = 0;
        for (int j = 1; j < size - i; ++j)
        {
            if (array[maxpos] < array[j])
            {
                maxpos = j;
            }
        }
        if (maxpos != size - i-1)
        {
            swap(&array[maxpos],&array[size - i-1]);
        }
    }
}
时间复杂度和空间复杂度

没有最好和最坏的情况,对于不管数组开始处于何种状态,必须进行1+2+….n=n(n-1)/2=O(n^2)次的比较
所以时间复杂度为O(n^2)
空间复杂度为O(1)
有什么不足吗??
每次比较的时候只选择最大的,可以对上述算法加以改进,每次选出最大的和最小的,这样比较的次数可以减小

void selectsort_op(int *array, int size)
{
    int begin = 0;
    int end = size - 1;
    while (begin < end)
    {
        int minpos = begin;
        int maxpos = begin;
        for (int j =begin + 1; j <= end ; j++)
        {
            if (array[minpos]>array[j])
            {
                minpos = j;
            }
            if (array[maxpos] < array[j])
            {
                maxpos = j;
            }
        }
        if (minpos != begin)
        {
            swap(&array[minpos], &array[begin]);
        }
        if (maxpos == begin)
        {
            maxpos = minpos;
        }
        if (maxpos != end)
        {
            swap(&array[maxpos], &array[end]);
        }
        begin++;
        end--;
    }
}

不分最好情况和最坏情况这样比较的次数就变成了n-1+n-3+…..+1=n(n-1)/4,当n较大时,效果比普通的选择排序的效果要好。

堆排序???????????
堆并排序

堆并排序的思想就是讲一个大的数组分成若干小的数组,在将这些小的数组依次排序,对这些以排好的小数组在进行合并。
在这里讲一下二路归并
这里写图片描述
当然依据二路归并还可以设计出多路归并

代码
void Mergetdata(int *array[], int left,int mid, int right, int  *tmp)
{
    int begin1 = left;
    int end1 = mid;
    int begin2 = mid + 1;
    int end2 = right;
    int index = left;
    while (begin1 <= end1&&begin2 <= end2)
    {
        if (array[begin1] < array[begin2])
        {
            tmp[index++] = array[begin1++];
        }
        else
        {
            tmp[index++] = array[begin2++];
        }
    }
    while (begin1 <= end1)
    {
        tmp[index++] = array[begin1++];
    }
    while (begin2 <= end2 )
    {
        tmp[index++] = array[begin2++];
    }
}


void _MergeSort(int *array[], int left, int right, int *tmp)
{
    if (left < right)
    {
        int mid = left + ((right - left) >> 1);
        _MergeSort(array, left, mid, tmp);
        _MergeSort(array, mid + 1, right, tmp);
        Mergetdata(array, left, mid, right, tmp);
        memcpy(array+left, tmp+left, sizeof(array[0])*(right - left + 1));
    }
}

void MergeSort(int *array[], int size)
{
    int *tmp = (int *)malloc(sizeof(array[0])*size);
    if (tmp == NULL)
        return;
    _MergeSort(array, 0, size-1, tmp);
    free(tmp);
}


int main()
{
    int array[] = { 19,13,5,27,1,26,31,16};
    MergeSort(array, sizeof(array) / sizeof(array[0]));
    print(array, sizeof(array) / sizeof(array[0]) );
    system("pause");
    return 0;
}

这里采用递归的思想,先从左递归直到排序的长度为2,即先排左array[0]-array[1],然后再排右array[2]-array[3],然后再左array[0]-array[3],在右array[4-7]的左array[4]-array[5],在右array[6]-array[7],在右array[4]-array[7],在总array[0]-array[7];

非递归
void MergeSortNor(int *array[], int size)
{
    int gap = 1;
    int *tmp = (int *)malloc(sizeof(array[0])*size + 1);
    if (tmp == NULL)
        return;
    while (gap < size)
    {
        for (int i = 0; i < size; i += 2 * gap)
        {
            int left = i;
            int right = i + gap+(gap-1);
            int mid = left + ((right - left) >> 1);
            Mergetdata(array, left, mid, right, tmp);
        }
        memcpy(array, tmp, sizeof(array[0])*size);
        gap *= 2;
    }
}

先两个两个一排,即array[0-1],arry[2-3],array[4-5],array[6-7],再四个四个一排,array[0-3],array[4-7],再八个一排array[0-7]


扩展

1.鸽巢原理
这里写图片描述
即将所有数据重新以计数的方式排列一遍,然后依次放在原来的数组中,进行覆盖,存储的数据已经不是原来的数了。

void CountSort(int* array, int size)
{
    int mindata;
    int maxdata;
    mindata = array[0];
    maxdata = array[0];
    int i = 0;
    //确定数据范围 O(N)
    for (i = 1; i < size; i++)
    {
        if (array[i] < mindata)
        {
            mindata = array[i];
        }
        if (array[i] > maxdata)
        {
            maxdata = array[i];
        }
    }
    int range = maxdata - mindata + 1;
    int *pcount = (int *)malloc(sizeof(int)*range);
    if (pcount == NULL)
    {
        return;
    }
    memset(pcount, 0, sizeof(int)*range);
    i = 0;
    //统计每个元素出现的次数O(N)
    for (i = 0; i < size; i++)
    {
        pcount[array[i] - mindata]++;
    }
    //回收数据
    i = 0;
    int index = 0;
    while (i < range)
    {
        while (pcount[i]--)
        {
            array[index++] = i + mindata;
        }
        i++;
    }
    free(pcount);
}
  • 时间复杂度,遍历数组O(n)
  • 空间复杂度,存放数据出现个数的空间O(maxdata-mindata+1)=O(M)
  • 适用于数据较少且集中分布的情况
利用哈希表(最低位优先法)

这里写图片描述
1.先获取数组中最大数的位数
2.获取count数组中取余相等的个数
3.通过count计算哈希表中每个元素的起始位置
3.按起始位置将元素依次放入哈希表中
4.依次从低位开始按位排序,第一次先将个位数排好,第二次将十位数排好,第三次将百位数排好…….

int GetBitNum(int *array[], int size)
{
    int i = 0;
    int count = 1;
    int index = 10;
    for (i; i<size; i++)
    {
        while (array[i]>index)
        {
            count++;
            index *= 10;
        }
    }
    return count;
}


void RadixSortLSD(int* array, int size)
{
    int i, bitdex, bitnum;
    bitnum = GetBitNum(array, size);
    int index = 1;
    int *bucket = (int *)malloc(sizeof(int)*size);
    if (bucket == NULL)
    {
        return;
    }
    for (bitdex = 0; bitdex < bitnum; bitdex++)
    {
        int count[10] = { 0 };
        int startaddr[10] = { 0 };
        //统计每个桶中元素的个数
        for (i = 0; i < size; i++)
        {
            count[array[i] / index % 10]++;
        }
        //计算起始位置
        for (i = 1; i < size; i++)
        {
            startaddr[i] = startaddr[i - 1] + count[i - 1];
        }
        //放入桶中
        for (i = 0; i < size; i++)
        {
            bucket[startaddr[array[i]/index%10]++] = array[i];
        }
        memcpy(array, bucket, sizeof(int)*size);
        index *= 10;
    }
    free(bucket);
}
int main()
{
    //int array[] = { 5, 8, 9, 12, 10, 8, 7, 10, 6 };
    int array[] = { 5, 9, 8, 15, 69, 53, 102, 134, 187, 126 };
    int size = sizeof(array) / sizeof(array[0]);
    RadixSortLSD(array, size);
    print(array, size);
    return 0;
}
  • 时间复杂度,位数*个数=O(M*N)
  • 空间复杂度O(N+2*10)=O(N)
  • 适用于数据元素分布不集中
最高位优先法

先找出比最高位低的第二位,(5, 9, 8, 15, 69, 53,)将其先按照从小到大一次排列,在将102, 134, 187, 126 按照从小到大依次排列

void _RadixSortMSD(int* array, int left, int right, int* bucket, int bit)
{
    int i;
    int count[10] = { 0 };
    int startAddr[10] = { left };
    int radix = (int)pow((double)10, bit);
    if (bit < 0)
        return;

    // 统计每个桶中有效元素的个数
    for (i = left; i < right; ++i)
        count[array[i] / radix % 10]++;

    // 计算每个桶的起始地址
    for (i = 1; i < 10; ++i)
        startAddr[i] = startAddr[i - 1] + count[i - 1];

    // 将元素按照当前位放置到对应的桶中
    for (i = left; i < right; ++i)
    {
        int bucketNo = array[i] / radix % 10;
        bucket[startAddr[bucketNo]++] = array[i];
    }

    // 对元素进行回收
    memcpy(array + left, bucket + left, (right - left)*sizeof(array[left]));
    for (i = 0; i < 10; ++i)
    {
        int begin = startAddr[i] - count[i];
        int end = startAddr[i];
        if (begin + 1 >= end)
            continue;

        _RadixSortMSD(array, begin, end, bucket, bit - 1);
    }
}

void RadixSortMSD(int* array, int size)
{
    int* bucket = (int*)malloc(sizeof(array[0])*size);
    if (NULL == bucket)
        return;

    _RadixSortMSD(array, 0, size, bucket, GetBitNum(array, size) - 1);

    free(bucket);
}

int main()
{
    //int array[] = { 5, 8, 9, 12, 10, 8, 7, 10, 6 };
    int array[] = { 5, 9, 8, 15, 69, 53, 102, 134, 187, 126 };
    int size = sizeof(array) / sizeof(array[0]);
    RadixSortMSD(array, size);
    print(array, size);
    return 0;
}
  • 时间复杂度:递归深度*递归次数=O(bitnum*N)=O(M*N)
  • 空间复杂度O(n)

总结

  • 简单排序算法适用于n比较小,当数据基本有序时,直接插入排序是最佳的算法,当数据较多时,则应采用移动次数较少的简单选择排序算法
  • **看书吧**354页
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值