数据结构总结

目录

一.各大排序算法:

1.排序的相关概念

(1)排序

(2)稳定性

(3)内部排序

(4)外部排序

2.常见排序介绍:

(1)插入类排序:

a.直接插入排序:

b.希尔排序:

(2)选择类排序

a.选择排序(优化版本)

b.堆排序:

(3)交换类排序

a.冒泡排序

b.快速排序

c.归并排序:

d.计数排序:


一.各大排序算法:

排序算法平均时间复杂度最差时间复杂度空间复杂度数据对象稳定性
冒泡排序O(n2)O(n2)O(1)稳定
选择排序O(n2)O(n2)O(1)数组不稳定、链表稳定
插入排序O(n2)O(n2)O(1)稳定
快速排序O(n*log2n)O(n2)O(log2n)不稳定
堆排序O(n*log2n)O(n*log2n)O(1)不稳定
归并排序O(n*log2n)O(n*log2n)O(n)稳定
希尔排序O(n*log2n)O(n2)O(1)不稳定
计数排序O(n+m)O(n+m)O(n+m)稳定
桶排序O(n)O(n)O(m)稳定
基数排序O(k*n)O(n2)O(k+n)稳定


1.排序的相关概念

(1)排序

所谓排序,就是使一串记录,
按照其中的某个或某些关键字的大小,递增或递减的排列起来的操作。

(2)稳定性

待排序数据在排序前后相同元素的相对位置是否发生改变
如果改变:该排序不稳定
反之就是稳定的

(3)内部排序

数据元素全部放在内存中的排序。

(4)外部排序

数据元素太多不能同时放在内存中,根据排序过程的要求不能在内外存之间移动数据的排序。

2.常见排序介绍:


(1)插入类排序:


a.直接插入排序:

算法思想

  1. 从第一个元素开始,该元素可以认为已经被排序。
  2. 取出下一个元素,在已经排序的元素序列中从后向前扫描。
  3. 如果该元素(已排序)大于新元素,该元素移到下一个位置。
  4. 重复步骤3,直到找到已排序的元素小于或者等于新的元素的位置。
  5. 将元素插入到对应位置。
  6. 重复2~5。

[1]主要思想:
{1}给定一组序列,默认第一个元素已经有序,从第二个元素开始遍历数组
{2}对于每一个待排序的元素,与已经排好序的元素进行从后向前的比较,
直到找到待插入元素的位置
{3}数组遍历完毕后排序也就完成
[2]代码实现:
//插入排序(升序)
void InsertSort(int array[], int size)
{
    for (int i = 1; i < size; i++)//控制循环趟数
    {
        int key = array[i];//标记待插入元素
        int end = i - 1;//end标记已经排好序的序列的最后一个下标
        while (end >= 0 && array[end] > key)
        {
            array[end + 1] = array[end];
            end--;
        }
        array[end + 1] = key;//找到插入元素的位置
    }
}
[2]复杂度分析
时间复杂度O(N^2)
两层循环,外层控制循环趟数,循环size-1次,
内层循环寻找插入位置,每个元素要找到自己的位置最差需要遍历已经排好序的所有元素,
因此要循环N次
综上,时间复杂度为O(N^2)
空间复杂度O(1):未借助辅助空间
[3]稳定性 :稳定
插入排序没有出现跨元素交换的情况,因此相同的元素排序前后相对位置不会发生变化
[4]适用场景:元素基本有序或元素数量比较少

b.希尔排序:

算法思想

  1. 选择一个增量序列t1,t2,…,tk,其中ti>tj,tk = 1。
  2. 按照增量序列个数k,对序列进行k趟排序
  3. 每趟排序根据对应的增量ti,将待排序的序列分割成若干长度为m的子序列,分别对各子表进行直接插入排序。

[1]主要思想:希尔排序是对直接插入排序的升级
{1}选定一个基准值,将数组下标%基准值相等的元素分为一组
{2}对每一组采用直接插入排序的方法进行排序
{3}对基准值进行更新,循环执行以上两步,直到基准值减小到1停止
{4}此时序列已经有序
[2]注:基准值如何选取?
采用Kunth提出的gap = gap/3 (向下取整)+1
[3]代码实现
//希尔排序
//定义gap,将数组下标%gap相等的元素分为一组
//对每一组采用插入排序的方式进行排序
//对gap每次执行 gap/3 +1 直到gap减小为1
void ShellSort(int array[],int size)
{
    int gap = size;
    //gap判断条件应该是大于1,因为gap的值始终是>=1的
    while (gap > 1)
    {
        gap = gap / 3 + 1;//gap等于1时,最后一次排序,
排完之后整个数组已经有序
        for (int i = gap; i < size; i++)
        {
            int key = array[i];
            int end = i - gap;
            while (end>=0 && array[end]>key)
            {
                array[end + gap] = array[end];
                end -= gap;
            }
            array[end + gap] = key;
        }
    }
}
[4]复杂度分析
时间复杂度
希尔排序的时间复杂度不好计算,因为gap的取值方法很多,导致很难去计算,
因此在好些树中给出的希尔排序的时间复杂度都不固定
在这里gap的计算采用的是Kunth的方法,这样对gap取值,
最终的时间复杂度位于N1.25~1.6*N1.25
空间复杂度
未借助任何辅助空间,所以是O(1)
[5]稳定性 :不稳定
存在跨区间交换元素
[6]适用场景:数据比较随机 数据量比较大

(2)选择类排序

a.选择排序(优化版本)

算法思想

  1. 在未排序的序列中找到最小(大)元素,存放到排序序列的起始位置。
  2. 从剩下未排序元素中继续寻找最小(大)元素,然后放到自己已排序的序列的末尾。
  3. 以此类推,直到所有元素排序完毕。

[1]主要思想:普通版本的选择排序一次只会从中选出一个元素(最大或最小)
优化后的版本,一次可以从带排序数组中选取出两个元素,分别是最大最小元素
具体步骤如下:
{1}以升序为例,遍历数组,找到数组中最大和最小的两个元素
{2}将最大元素与数组末尾元素交换,将最小元素与数组开头元素交换
{3}循环,直到对数组完成一次遍历后,排序结束
[2]代码实现 原始版本+优化版本
//选择排序
static void Swap(int *left, int *right)
{
    int temp = *left;
    *left = *right;
    *right = temp;
}

void SelectSort(int array[], int size)
{

    for (int i = 0; i < size-1; i++)
//控制循环趟数 size个元素,需要循环size-1趟
    {
        int maxPos = 0;
        for (int j = 0; j < size - i; j++)
//每一趟遍历数组中未排序元素,更新maxPos的位置
        {
            if (array[maxPos] < array[j])
            {
                maxPos = j;
            }
        }
        if (maxPos != size - 1 - i)
        {
            Swap(&array[maxPos], &array[size - 1 - i]);
        }
    }
}

//优化版本
void SelectSortOP(int array[], int size)
{
    int left = 0;
    int right = size - 1;
    while (left < right)
    {
        int maxPos = left;
        int minPos = left;

        int index = left + 1;
        while (index <= right)
        {
            if (array[maxPos] < array[index])
            {
                maxPos = index;
            }
            if (array[minPos]>array[index])
            {
                minPos = index;
            }
            index++;
        }
        if (maxPos != right)
        {
            Swap(&array[maxPos],&array[right]);
        }

        if (minPos == right)
        {
            minPos = maxPos;
        }

        if (minPos != left)
        {
            Swap(&array[minPos],&array[left]);
        }
        left++;
        right--;
    }
}
[3]复杂度分析
时间复杂度 O(N2)
空间复杂度 O(1)
[4]稳定性:不稳定
选择排序存在跨区间交换
[5]适用场景:基本情况都可以使用,但都不使用(由于时间复杂度的原因)

b.堆排序:

算法思想

  1. 如果要从小到大排序,建立大堆,根节点大于左右子树。
  2. 将根结和最后一个元素交换,并且树的元素个数减1。
  3. 重复1~2,直到只剩一个元素。

[1]主要思想
{1}建堆:将待排序元素按照要求进行建堆操作
     升序:建大堆
     降序:建小堆
{2}利用堆删除的思想进行排序
将堆顶元素与最后一个元素交换,将堆元素规模-1,
然后对新的堆顶元素采用向下调整算法是其称为新的堆结构
[2]代码实现:
//堆排序

static void HeapAdjust(int array[], int size, int parent)
{
    int child = 2 * parent + 1;
//默认标记左孩子(完全二叉树,必定是先有左孩子,再有右孩子)
    while (child < size)
    {
        if (child + 1 < size && array[child + 1] > array[child])
//更新child,让其指向较大的孩子结点
        {
            child += 1;
        }
        if (array[child]>array[parent])  
//判断较大孩子结点与父节点的大小关系
        {
            Swap(&array[child], &array[parent]);
//交换后更新parent与child,因为交换可能会导致子树不符合堆的结构
            parent = child;
            child = 2 * child + 1;
        }

        else//满足堆的结构
        {
            return;
        }
    }
}

void HeapSort(int array[], int size)
{
    //1.建立堆(向下调整)  升序--大堆   降序---小堆
    for (int root = (size - 2) / 2; root >= 0; root--)
    {
        HeapAdjust(array, size, root);
    }
    //2.利用堆删除的思想进行排序
    int end = size - 1;
    while (end > 0)
    {
        Swap(&array[0], &array[end]);
        HeapAdjust(array, end, 0);
        end--;
    }
}
[3]复杂度分析
时间复杂度:O(NlogN)
空间复杂度:O(1)
[4]稳定性 : 不稳定
[5]适用场景 : Top-K问题

(3)交换类排序

a.冒泡排序

算法思想

  1. 比较相邻的元素,如果第一个比第二个大,就交换他们两个。
  2. 对每一对相邻元素作同样的工作,从第一对开始,一直到最后一对,做完后,最后的元素会是最大的元素。
  3. 针对所有的元素重复上面的步骤,除排序好的。
  4. 持续对越来越少的元素重复上述步骤,直到哪次没有任何一对数字需要比较或者是交换。

[1]主要思想
以升序为例:
给定一组数据,相邻元素之间两两进行比较,一趟下来,将最大的元素排到了数组最后,
循环size-1趟后,数组整体将会有序。在每一趟循环过程中,定义一个标志量,
用来标记本趟排序是否发生交换,如果发生,说明数组没有完全有序,继续下一趟循环,
如果没有发生改变,说明数组已经有序,可以提前跳出循环
[2]代码实现
//冒泡排序
//两两比较,一趟遍历找到一个元素的最终位置(最大或最小)
void BubbleSort(int array[],int size)
{
    for (int i = 0; i < size - 1; i++)
//控制循环趟数,size个元素循环size-1次即可完成排序
    {
        int isChanged = 0;
//标记一趟遍历中是否有元素交换,若没有交换,说明数组已经有序。
此时直接返回,无需再进入下面的循环
        for (int j = 0; j < size - 1 - i; j++)
//将最大元素搬移至数组最后
        {
            if (array[j]>array[j + 1])
            {
                isChanged = 1;
                Swap(&array[j], &array[j + 1]);
            }
        }
        if (!isChanged)
        {
            return;
        }
    }
}
[3]复杂度分析:
时间复杂度:O(N2)
空间复杂度:O(1)
[4]稳定性 :稳定
[5]适用场景:数据接近有序

b.快速排序

算法思想

  1. 选第一个数为标准。
  2. 将比基准小的数据交换到前面,比基准大的交换到后面
  3. 对左边的空间和右边的空间重复,直到各区间只有一个数字

递归版本:
[1]主要思想:
{1}选定一个基准值,
将该组数据小于基准值的划分子在基准值的左侧,大于基准值的划分在基准值的右侧
{2}递归:
使用快排对基准值的左侧进行排序
使用快排对基准值的右侧进行排序
[2]代码实现:
//递归
void QuickSort(int array[], int left, int right )
{
    if (right - left > 1)
    {
        //根据Partion对数组进行划分--->小于div的位于div的左侧,
                   大于等于div的位于div的右侧
        //主要划分方式有 hoare版本  挖坑法  前后指针法
        int div = Partion(array, left, right);

        QuickSort(array, left, div);

        QuickSort(array, div + 1, right);
    }
}
[3]划分方法&基准值确定
上述说法中有两个点值得深究:
{1}如何对数据划分
首先,有三种比较常见的划分方式,分别是hoare版本、挖坑法、前后指针法
a.hoare版本
[1]让begin从前向后遍历,找到大于基准值的元素停下来
[2]让end从后向前遍历,找到小于基准值的元素停下来
[3]将begin和end所指向的元素交换
[4]循环1、2、3步,直到begin与end相遇,划分结束
代码展示:
 //hoare版本  
 static int Partion1(int array[], int left, int right)
 {
     int begin = left;
     int end = right - 1;
     int mid = GetMidPos(array, left, right);
     if (mid != end)
     {
         Swap(&array[end], &array[mid]);
     }
    
     int key = array[end];
     while (begin < end)
     {
         while (begin < end&& array[begin] <= key)
         {
             begin++;
         }
         while (begin<end && array[end]>= key)
         {
             end--;
         }
         if (begin < end)
         {
             Swap(&array[begin], &array[end]);
         }
     }
     if (begin != right-1)
     {
         Swap(&array[begin], &array[right-1]);
     }

     return begin;
 }
挖坑法:
主要思想与hoare版本相似
[1]前提:将基准值保存到key中,此时数组基准值所在位置相当于可以占的“坑位”,
用end标记
a.begin从前向后遍历,找到大于基准值key的元素后停止,
然后将该元素赋值给空着的坑位end,此时,begin所指的位置称为新的坑位
b.end指针从后向前移动,遇到小于基准值key的元素后,将其赋值给新的坑位begin
c.循环执行上述操作,直到begin与end相遇时,
将key值赋值到begin所在位置,一次划分结束
[4]过分过程中的基准值如何选取:
采用三数取中法,即:取数组左右端点以及中间元素三者中值居中的作为基准值
注:一般情况下找到的基准值要是不在数组末尾,
将基准值与末尾元素交换(确保基准值始终在数组的末尾)

非递归版本:
[1]主要思想
{1}与递归的思路基本一致,只不过是采用栈这一数据结构来存储待处理元素的下标范围
{2}先存储右区间,再存储左区间。每个区间先存储右边界,再存储左边界
[2]代码实现
#include "Stack.h"
//非递归
void QuickSortNor(int array[], int size)
{
    int begin = 0;
    int end = size;

    Stack s;
    StackInit(&s);
    StackPush(&s, end);

    StackPush(&s, begin);

    while (!StackEmpty(&s))
    {
        begin = StackTop(&s);
        StackPop(&s);
        end = StackTop(&s);
        StackPop(&s);
        if (end - begin > 1)
        {
            //div将数组分为左右两部分
            int div = Partion(array, begin, end);

            //按照先处理左边后处理右边的思想将边界下标入栈
            //入栈顺序为先入右后入左
            StackPush(&s, end);
            StackPush(&s, div + 1);
            StackPush(&s, div);
            StackPush(&s, begin);
        }
    
    }
    //栈为空,表示排序完成,将栈销毁
    StackDestroy(&s);
}
[3]复杂度分析
时间复杂度:O(NlogN)
空间复杂度:O(logN)
[4]稳定性 : 不稳定
[5]适用场景:接近有序 | 求数组中第k大(小)的数据

c.归并排序:

算法思想

  1. 把长度为n的输入序列分成两个长度为n/2的子序列;
  2. 对这两个子序列分别采用归并排序;
  3. 将两个排序好的子序列合并成一个最终的排序序列。

递归:
[1]主要思想:
{1}均分,将待排序数组不断均分,分为左右两半部分,直到每一部分都是有序
(仅有一个元素)
{2}归并。情形:合并两个有序数组
{3}将存储在临时数组中的元素copy至原数组
[2]代码实现:
//归并排序

static void MergeData(int array[], int left, int mid, 
int right, int temp[])
{
    int begin1 = left;
    int end1 = mid;
    int begin2 = mid;
    int end2 = right;
    int index = left;
    while (begin1 < end1 && begin2 < end2)
    {
        if (array[begin1] <= array[begin2])
        {
            temp[index++] = array[begin1++];
        }
        else
        {
            temp[index++] = array[begin2++];
        }
    }
    while (begin1 < end1)
    {
        temp[index++] = array[begin1++];
    }
    while (begin2 < end2)
    {
        temp[index++] = array[begin2++];
    }
}

static void _MergeSort(int array[], int left, int right,int temp[])
{
    int mid = left + ((right - left) >> 1);
    if (right - left > 1)
    {
        //分解
        _MergeSort(array, left, mid,temp);
        _MergeSort(array, mid, right,temp);

        //合并
        MergeData(array,left,mid,right,temp);

memcpy(array + left, temp + left, (right - left)*sizeof(array[0]));
    }
}

//递归

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

非递归:
[1]主要思想:
{1}将待排序数组的每一个元素看做是一个独立的个体,那么他们每个都是有序的
{2}对每一个部分直接归并,
而直接归并分三步,分别是
①采用gap来标记每次每组参与归并的元素个数
②gap从1开始,每归并完成一次,gap*2
③直到gap的值大于数组元素个数size时,说明排序完成
注:随着gap的不断增大,归并函数中边界变量可能会越界,需要做合法性判断
{3}每归并一次,将临时数组中的元素拷贝至原数组
[2]代码实现:
//非递归
void MergeSortNor(int array[], int size)
{
    int* temp = (int*)malloc(sizeof(array[0])*size);
    if (NULL == temp)
    {
        assert(0);
        return;
    }

    int gap = 1;
    while (gap < size)
    {
        for (int i = 0; i < size; i+= 2*gap)
        {
            int left = i;
            int mid = left + gap;
            int right = mid + gap;

            //mid 与right可能会越界,需要进行合法性检验
            if (mid > size)
            {
                mid = size;
            }
            if (right > size)
            {
                right = size;
            }
            //默认从一个元素为一组进行归并
            MergeData(array, left, mid, right, temp);
        }
        //将临时数组temp中的元素copy至array数组
        memcpy(array, temp, sizeof(array[0])*size);
        gap *= 2;
    }
    free(temp);
}
[3]复杂度分析
时间复杂度:O(NlogN)
空间复杂度:O(N)
[4]稳定性:稳定
[5]适用场景 :数据量大,无法一次加载到内存 外部排序

d.计数排序:

算法思想

  1. 找出待排序的数组最大和最小的元素
  2. 统计数组中每个值为i的元素出现的个数,存入数组c的第i-min项
  3. 将下标+min的值根据在数组c中的个数存到原数组中。

[1]主要思想:
{1}统计待排序区间中每个元素出现的次数,
{2}通过铺助数组count来保存,即每出现一个待排序元素array[i],
将对应的count[array[i]]加1
{3}循环遍历数组count,数组中的值count[i]表明数据i出现的次数,
数据出现几次,就输出几个i
[2]代码实现
// 计数排序
void CountSort(int* array, int size)
{
    //1、确定辅助数组大小
    int maxValue = array[0];
    int minValue = array[0];
    for (int i = 1; i < size; i++)
    {
        if (array[i]>maxValue)
        {
            maxValue = array[i];
        }
        if (array[i] < minValue)
        {
            minValue = array[i];
        }
    }
    int len = maxValue - minValue + 1;
    int *count = (int*)malloc(sizeof(int)*len);
    if (NULL == count)
    {
        assert(0);
        return;
    }
    memset(count, 0, sizeof(int)*len);
    //2、遍历待排序数组,统计每个元素出现的次数
    for (int i = 0; i < size; i++)
    {
        count[array[i] - minValue]++;
    }
    //3、遍历辅助数组,按照数组值输出对应个数的元素
    int index = 0;

    for (int i = 0; i < len; i++)
    {
        while (count[i]--)
        {
            //printf("%d ", i + minValue);
        
            array[index++] = i + minValue;
        }
    }
    printf("\n");
    //4、释放辅助空间
    free(count);
}
[3]复杂度:
时间复杂度O(N)
空间复杂度O(M),M表示所给数据的范围,即:maxVale-minval
[4]稳定性:稳定
[5]适用场景:数据集中在菜一个数据段

桶排序

算法思想

  1. 设置一个定量的数组当作空桶;
  2. 遍历输入数据,并且把数据一个一个放到对应的桶里去;
  3. 对每个不是空的桶进行排序;
  4. 从不是空的桶里把排好序的数据拼接起来。

基数排序

算法思想

  1. 取得数组中的最大数,并取得位数。
  2. arr为原始数组,从最低位开始取每个位组成radix数组。
  3. 对radix进行计数排序(利用计数排序适用于小范围数的特点)。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

墨柟

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值