数据结构-排序算法

目录

[一. 算法](#一. 算法)

[1.1 算法概念](#1.1 算法概念)

[1.2 算法设计](#1.2 算法设计)

[1.3 算法的时间复杂度](#1.3 算法的时间复杂度)

[1.3.1 时间复杂度概念:](#1.3.1 时间复杂度概念:)

[1.3.2 时间复杂度的计算规则:](#1.3.2 时间复杂度的计算规则:)

[二. 排序和查找算法](#二. 排序和查找算法)

[2.1 排序算法](#2.1 排序算法)

[2.1.1 插入排序](#2.1.1 插入排序)

1、直接插入排序(insertion_sort)

2、希尔排序(shell_sort)

3、直接插入排序与希尔排序的对比

[2.1.2 选择排序](#2.1.2 选择排序)

1、选择排序(selection_sort)

2、堆排序

向下调整算法:

[2.1.3 交换排序](#2.1.3 交换排序)

1、冒泡排序(bubble_sort)

2、快速排序(quick_sort)

方法一:挖坑法

[2.1.4 归并排序](#2.1.4 归并排序)

[2.2 查找算法](#2.2 查找算法)

[2.2.1 二分查找](#2.2.1 二分查找)

一. 算法

1.1 算法概念

1、算法:算法是解决问题的一系列明确指令的集合,能够在有限时间内对给定的输入产生所

需的输出。算法的优劣可以通过时间复杂度空间复杂度来衡量。

2、程序设计 = 数据结构 + 算法

3、**算法:**对数据操作的流程步骤

1.2 算法设计

1、正确性:语法正确

合法的输入可以得到合理的结果

对非法的输入,可以给出满足要求的规格说明

对所有的测试都可以正常运行,结果正确

2、高内聚,低耦合,可读性要高

3、算法健壮性要好,输入非法的数据,可以给出相应的处理,而不是出现异常

4、高效率 (时间复杂度)、低存储(空间复杂度)

1.3 算法的时间复杂度

1.3.1 时间复杂度概念:

时间复杂度:执行这个算法所花费时间的度量

时间复杂度一般用 **O()**表示:O(n)

时间复杂度是n的函数,随着n的增加,时间复杂度增长较慢的算法时间复杂度低

1.3.2 时间复杂度的计算规则:

1、用常数1取代运行时间中的所有加法常数

2、只保留最高阶项

3、如果最高阶存在且系数不为1,则去除这个项相乘的函数

二. 排序和查找算法

1、排序算法的稳定性 :在待排序列中,出现了两个相同数据,经过排序,这两个相同数据的

相对位置没有发生变化 ,该排序算法就是一个稳定的排序算法。不稳定的排序指的是在排

序过程中,若两个相等的元素在排序后可能改变其相对顺序,则该算法被认为是不稳定

2.1 排序算法

2.1.1 插入排序

基本思想:把待排序的记录按其关键码值的大小逐个插入到一个已经排好序的有序序列中,直到所有的记录插入完为止,得到一个新的有序序列

1、直接插入排序(insertion_sort)

基本思想: 当插入第i(i>=1)个元素时,前面的array0,array1,...,arrayi-1已经排好序,此时用arrayi的排序码与arrayi-1,arrayi-2,...的排序码顺序进行比较,找到插入位置即将arrayi插入,原来位置上的元素顺序后移。**注:**当只有一个元素时,默认有序,所有应从第二个元素开始插入

直接插入排序的特性总结:

  1. 元素集合越接近有序,直接插入排序算法的时间效率越高

  2. 时间复杂度:O(N^2)

  3. 空间复杂度:O(1),它是一种稳定的排序算法

  4. 稳定性:稳定

代码实现:

cpp 复制代码
//直接插入排序
void insertion_sort(int *arr, int size)
{
    int i;
    for(i = 0; i < size - 1; i++){
        int j = i + 1;
        int temp = arr[j];
        while(j > 0 && arr[j - 1] > temp){
            arr[j] = arr[j - 1];
            j--;
        }
        arr[j] = temp;
    }
}
2、希尔排序(shell_sort)

又称缩小增量排序:将待排序列分成若干个子序列,分别对这若干个子序列进行插入排序。

**基本思想:**先选定一个整数,把待排序文件中所有记录分成n个组,所有距离为n的记录分在同一组内,并对每一组内的记录进行排序。然后,取重复上述分组和排序的工作。当到达n=1时,所有记录在统一组内排好序

eg:

希尔排序的特性总结:

  1. 希尔排序是对直接插入排序的优化。

  2. 当gap > 1时都是预排序 ,目的是让数组更接近于有序。当gap == 1时,数组已经接近有序,这样就会很快。这样整体而言,可以达到优化的效果。

  3. 希尔排序的时间复杂度: O(nlogn) ~ O(n^2)

  4. 稳定性:不稳定---( 因为分组是跨元素的远距离交换**)**

代码实现:

cpp 复制代码
//希尔排序
void shell_sort(int *arr, int size)
{
    int gap = size;
    while(gap > 1){
        gap /= 2;//初始的间隔就等于数组数据个数的一半
        int i;
        for(i = 0; i < size - gap; i++){
            int j = i + gap;
            int temp = arr[j];
            while(j >= gap && arr[j - gap] > temp){
                arr[j] = arr[j - gap];
                j -= gap;
            }
            arr[j] = temp;
        }
    }
}
3、直接插入排序与希尔排序的对比

对直接插入排序与希尔排序进行对比,更好的说明希尔排序的思想,如下图所示:

1、最关键的一点就是图中绿色框住 的部分:直接插入排序i的上限是i < size - 1 ;而希尔排序i的上限是i < size - gap ;

解释:单个元素可以认为其本来就是有序的,不用进行排序

在直接插入排序中:可以认为第一个元素有序,不用对其排序,故总共排序次数为 size - 1

在希尔排序中:由于对数据进行了gap为间隔的分组,总共分为了gap组 ,每组的第一个元素都默认有序,故总共默认有序的元素为gap个,故总共排序次数为 size - gap

2、为什么:对数据进行了gap为间隔的分组,总共分为了gap组

原因:下标 % gap 的余数只有 0,1,2,...,gap−1,一共 gap 种,每种对应一组,所以一定是

gap 组

3、下标 % gap 的余数只有 0,1,2,...,gap−1,一共 gap 种,每种对应一组,所以一定是gap 组

eg:10个数,gap = 3

2.1.2 选择排序

选择排序的基本思想:每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到全部待排序的数据元素排完 。

1、选择排序(selection_sort)

**基本思想:**每次遍历选取一个最小值将其存放到序列的起始位置。

时间复杂度:O(n^2)

空间复杂度:O(1)

稳定性:不稳定---( eg:对" 3 2 3 1 " 进行选择排序会改变两个3的相当位置**)**

代码实现:

cpp 复制代码
//选择排序
void selection_sort(int *arr, int size)
{
    int i, j;
    for(i = 0; i < size - 1; i++){
        int min_ind = i;
        for(j = i + 1; j < size; j++){
        //选出最小值对应的下标
            if(arr[j] < arr[min_ind]){
                min_ind = j;
            }
        }
        if(min_ind != i){
            swap(&arr[i], &arr[min_ind]);
        }
    }
}

进一步优化:双向选择排序(鸡尾酒选择排序)

基本思想:每次遍历选取最小值最大值,将其存放到序列的对应位置

cpp 复制代码
//双向选择排序
void selection_sort1(int *arr, int size)
{
    int left = 0, right = size - 1;
    while(left < right){
        int min = left, max = right;
        for(int i = left; i <= right; i++){
        //找出当前区间的最大与最小值下标
            if(arr[i] < arr[min]){
                min = i;
            }
            if(arr[i] > arr[max]){
                max = i;
            }
        }
        swap(&arr[left], &arr[min]);
        if(max == left){
        //如果left=max则上一步将最大值交换到了min下标处,此处应该修正
            max = min;
        }
        swap(&arr[right], &arr[max]);
        left++;
        right--;
    }
}
2、堆排序

堆排序使用堆来选数,效率就高了很多。

时间复杂度:O(n*logn)

空间复杂度:O(1)

稳定性:不稳定

1、二叉树的表示方法有两种:链式与数组

2、堆的逻辑结构是一个完全二叉树

物理结构是数组:可以通过下标的关系来寻找一个节点的子节点或者父节点

3、堆的两个特性:

结构性:用数组表示的完全二叉树

有序性:任一节点的值是其子树中所有节点的最大值(最小值)

最大值->最大堆(maxheap) 最小值->最小堆(minheap)

4、 要想采用堆排序必须先建堆,排升序建大堆,排降序建小堆,建堆采用向下调整算法

向下调整算法:

1、前提:左右子树都是大/小堆

2、算法思想:若是建大堆,则从根节点开始,在左右孩子节点中找出最大的那个,若此节点比根节点的值大,则交换两者的值,然后从该节点继续向下调整,直到结束。建小堆同理。

eg:建小堆过程:

3、若不满足前提怎么办:采用自底向上的建堆方法即可。

如何找出最后一个不为叶子节点的节点:使用堆的结构性,用最后一个节点的下标计算出其对应的父节点即可。

4、向下调整算法代码实现:

cpp 复制代码
//向下调整算法
void adjust_down(int *arr, int size, int root)
{
    int parent = root;
    int child = parent * 2 + 1;//默认左孩子是两个孩子中最小的
    while(child < size){
        //建大堆建小堆改变>的方向即可
        if(child + 1 < size && arr[child + 1] > arr[child]){
            child++;
        }
        if(arr[child] > arr[parent]){
            swap(&arr[child], &arr[parent]);
        }
        else{
            break;//如果左右孩子都比父节点小,说明已经调整成功,跳出循环即可
        }
        parent = child;
        child = parent * 2 + 1;
    }
}

5、建堆代码示例:

cpp 复制代码
//堆排序
void heap_sort(int *arr, int size)
{
    //先建堆
    int i = 0;
    for(i = (size - 1 - 1) / 2; i >= 0; i--){
        adjust_down(arr, size, i);
    }
    //排序
}

6、为什么排升序建大堆,排降序建小堆

如果排升序采用建小堆的方式,则在第一次选择出最小的值后,堆的结构被破坏,再选出次小的数还得再建队,导致时间复杂度增加

若采用建大堆:

先在堆顶找出最大值,与最后一个数交换,这时并不破坏堆结构,只对堆顶元素做一个向下调整算法即可找出次大值,重复此步骤直到排序结束。

堆排序代码示例:

cpp 复制代码
//向下调整算法
void adjust_down(int *arr, int size, int root)
{
    int parent = root;
    int child = parent * 2 + 1;//默认左孩子是两个孩子中最小的
    while(child < size){
        //建大堆建小堆改变>的方向即可
        if(child + 1 < size && arr[child + 1] > arr[child]){
            child++;
        }
        if(arr[child] > arr[parent]){
            swap(&arr[child], &arr[parent]);
        }
        else{
            break;//如果左右孩子都比父节点小,说明已经调整成功,跳出循环即可
        }
        parent = child;
        child = parent * 2 + 1;
    }
}

//堆排序
void heap_sort(int *arr, int size)
{
    //先建堆
    int i = 0;
    for(i = (size - 1 - 1) / 2; i >= 0; i--){
        adjust_down(arr, size, i);
    }
    //排序
    int end = size - 1;
    while(end > 0){
        swap(&arr[0], &arr[end]);
        adjust_down(arr, end, 0);
        end--;
    }
}

2.1.3 交换排序

交换排序的基本思想:所谓交换,就是根据序列中两个记录键值的比较结果来对换这两个记录在序列中的位置,交换排序的特点是:将键值较大的记录向序列的尾部移动,键值较小的记录向序列的前部移动

1、冒泡排序(bubble_sort)

eg:对 25、6、56、24、9、12、55 这7个数进行冒泡排序

1、第一趟排序,将最大值交换到最后,第二趟排序,将次大值交换的倒数第二个位置,循环。

2、对于 n 个数,总共要进行 n-1趟排序

3、第 i 趟排序,总共进行n-i次比较

时间复杂度:O(n^2)

空间复杂度:O(1)

稳定性:稳定

代码实现:

cpp 复制代码
//两数交换
void swap(int *e1, int *e2)
{
    int temp = *e1;
    *e1 = *e2;
    *e2 = temp;
}
//冒泡排序
void bubble_sort(int *arr, int size)
{
    int i = 0, j = 0;
    int flag = 0;
    for(i = 1; i < size; i++){
        flag = 0;//0代表没有发生交换
        for(j = 0; j < size - i; j++){
            if(arr[j] > arr[j + 1]){
                swap(&arr[j], &arr[j + 1]);
                flag = 1;//发生了交换
            }
        }
        //没有发生交换则已经有序
        if(!flag){
            break;
        }
    }
}
2、快速排序(quick_sort)

快速排序基本思想: 任取待排序元素序列中的某元素作为基准值(key),按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止

特性:

时间复杂度:O(nlogn)

空间复杂度:O(nlogn) ----函数递归产生栈帧

稳定性:不稳定---( eg:对 "4 3 4 2" 进行快排会改变两个4的相当位置**)**

eg:

将区间按照基准值划分为左右两半部分的常见方式有:

挖坑法 前后指针法 双指针法

方法一:挖坑法

先将第一个数据存放在临时变量key中,形成一个坑位,先从右边开始找(因为一开始选取最左边的数为基准值),将比基准值大的存在后面的序列,比基准值小的存在前面的序列;经过一趟排序,将基准值存入合适的位置。并且以基准值为界,划分左右序列继续按照以上方式排序。

代码实现:

cpp 复制代码
//快速排序
void quick_sort1(int *arr, int left, int right)
{
    if(left >= right){
        return;
    }
    int key = arr[left];
    int i = left, j = right;
    while(i < j){
        //右找小
        while(i < j && arr[j] >= key){
            j--;
        }
        arr[i] = arr[j];
        //左找大
        while(i < j && arr[i] <= key){
            i++;
        }
        arr[j] = arr[i];
    }
    arr[i] = key;
    quick_sort1(arr, left, i - 1);
  

这里默认使用数组最左边的元素作为基准值 ,这样做有一个缺点

1、当数据本来有序时,这时的基准值每次取的都是数组的最小(最大)值,这样导致快排每次只排好一个数据的位置,类似选择排序,时间复杂度为O(n^2)

2、当数据本来无序时,每次取基准值都取最左边的数据,恰好每次最左边的数据又是这组数据的最大(最小)值,时间复杂度为O(n^2)

解决方法:三数取中

从数据的 left right mid 这三个下标中取大小为中间的那个数,保证了不会出现最坏情况。

(mid = (right - left) / 2 + left)

三数取中代码实现:

cpp 复制代码
//三数取中
int get_mid_num(int *arr, int left, int right)
{
    int mid = (right - left) / 2 + left;
    if(arr[left] < arr[right]){
        if(arr[mid] > arr[right]){
            return right;
        }
        else if(arr[mid] < arr[left]){
            return left;
        }
        else{
            return mid;
        }
    }
    //arr[right] <= arr[left]
    else{
        if(arr[mid] > arr[left]){
            return left;
        }
        else if(arr[mid] < arr[right]){
            return right;
        }
        else{
            return mid;
        }
    }
}

优化后的挖坑法代码:

cpp 复制代码
//三数取中
int get_mid_num(int *arr, int left, int right)
{
    int mid = (right - left) / 2 + left;
    if(arr[left] < arr[right]){
        if(arr[mid] > arr[right]){
            return right;
        }
        else if(arr[mid] < arr[left]){
            return left;
        }
        else{
            return mid;
        }
    }
    //arr[right] <= arr[left]
    else{
        if(arr[mid] > arr[left]){
            return left;
        }
        else if(arr[mid] < arr[right]){
            return right;
        }
        else{
            return mid;
        }
    }
}
//快速排序
void quick_sort(int *arr, int left, int right)
{
    if(left >= right){
        return;
    }
    int mid_index = get_mid_num(arr, left, right);//三数取中
    swap(&arr[left], &arr[mid_index]);//将中间大的数交换到最左边

    int pit = left;//坑位
    int key = arr[left];//key值
    int l_id = left, r_id = right;
    while(l_id < r_id){
        //右找小
        while(l_id < r_id && arr[r_id] >= key){
            r_id--;
        }
        arr[pit] = arr[r_id];
        pit = r_id;
        //左找大
        while(l_id < r_id && arr[l_id] < key){
            l_id++;
        }
        arr[pit] = arr[l_id];
        pit = l_id;
    }
    arr[pit] = key;
    quick_sort(arr, left, pit - 1);
    quick_sort(arr, pit + 1,right);
}

2.1.4 归并排序

时间复杂度:O(n*logn)

空间复杂度:O(n)

稳定性:稳定

基本思想:归并排序(MERGE-SORT)是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二

路归并。

归并排序核心步骤:

代码实现:

cpp 复制代码
//归并排序子函数
void _merge_sort(int *arr, int left, int right, int *tmp)
{
    if(left >= right){
        return;
    }
    int mid = (right - left) / 2 + left;
    _merge_sort(arr, left, mid, tmp);
    _merge_sort(arr, mid + 1, right, tmp);
    int begin1 = left, end1 = mid;
    int begin2 = mid + 1, end2 = right;
    int index = left;
    while(begin1 <= end1 && begin2 <= end2){
        if(arr[begin1] < arr[begin2]){
            tmp[index++] = arr[begin1++];
        }
        else{
            tmp[index++] = arr[begin2++];
        }
    }
    while(begin1 <= end1){
        tmp[index++] = arr[begin1++];
    }
    while(begin2 <= end2){
        tmp[index++] = arr[begin2++];
    }
    for(int i = left; i <= right; i++){
        arr[i] = tmp[i];
    }
}

//归并排序
void merge_sort(int *arr, int size)
{
    int *tmp = malloc(sizeof(int)*size);
    if(tmp == NULL){
        printf("malloc error\n");
        exit(1);
    }
    _merge_sort(arr, 0, size - 1, tmp);
    free(tmp);
    tmp = NULL;
}

排序算法对比:

2.2 查找算法

2.2.1 二分查找

**前提条件:**必须是一个有序的序列

时间复杂度:O(logn)

代码实现:

cpp 复制代码
//二分查找
int bin_find(int *arr, int size, int target)
{
    int count = 0;//统计查找次数
    int left = 0, right = size - 1;
    while(left <= right){
        count++;
        int mid = (right - left) / 2 + left;//防止溢出
        if(arr[mid] == target){
            printf("conut is %d\n",count);
            printf("find : ind is %d,num is  %d\n",mid,arr[mid]);
            return 0;
        }
        else if(arr[mid] < target){
            left = mid + 1;
        }
        else{
            right = mid - 1;
        }
    }
    printf("conut is %d\n",count);
    printf("find fail\n");
    return -1;
}
相关推荐
过期动态1 小时前
【LeetCode 热题 100】无重复字符的最长子串
java·数据结构·spring boot·算法·leetcode·职场和发展
莫等闲-2 小时前
leetcode42. 接雨水 leetcode84.柱状图中最大的矩形
数据结构·c++·算法·leetcode
unicrom_深圳市由你创科技2 小时前
历史数据存储量太大,怎么处理?数据压缩/归档策略?
算法
浅念-2 小时前
LeetCode 记忆化搜索 刷题总结
数据结构·算法·leetcode·职场和发展·深度优先·dfs
菜菜的顾清寒2 小时前
力扣HOT100(44)对称二叉树
数据结构·算法·leetcode
六bring个六2 小时前
c/c++面试踩坑笔记
c语言·数据结构·c++
吃好睡好便好3 小时前
矩阵的左乘和右乘
人工智能·学习·线性代数·算法·matlab·矩阵
我命由我123453 小时前
SEO 与 GEO 极简理解
java·linux·运维·开发语言·学习·算法·运维开发
月光刺眼3 小时前
🎶二分 · 双指针 · 滑动窗口 · 螺旋矩阵:数组算法四题拆解
javascript·算法