算法-查找、排序算法

一、算法和算法分析

算法:

​ 对特定问题的求解步骤的一种描述,它是指令的有限序列,其中每一条指令表示一个或多个操作。

算法的五个重要特性:

  • 有穷性:算法在执行有限的步骤后,自动结束而不会出现无限循环,而且每一个步骤在可接受的时间内完成。
  • 确定性:算法的每一个步骤都具有确定的含义,不会出现二义性。
  • 可行性:每一步都必须是可行的。
  • 输入:一个算法有零个或多个输入,以刻画运算对象的初始情况,所谓零个输入是指算法本身给定了初始条件。
  • 输出:一个算法有一个或多个输出。没有输出的算法毫无意义。

算法设计的要求:

  • 正确性:算法至少应该具有输入、输出和加工处理无歧义性、能正确放映问题的需求、能够得到问题的正确答案。
  • 可读性:便于阅读、理解和交流。
  • 健壮性:当输入数据不合法,算法也能做出相应处理。
  • 高效率与低存储:即算法执行的时间和算法执行的过程中需要的最大存储空间(算法程序运行时所占用的内存或外部硬盘存储空间)

算法效率的度量:

​ 算法的执行时间需要依据该算法所编制的程序在计算机上运行所消耗的时间来度量,一般有两种方法:

事后统计的方法:

​ 很多计算机内部都有计时功能,不同的算法的程序可通过一组或若干相同的统计数据以分辨优劣,例如:time ./a.out,但这种方法有两个缺陷,一是必须先运行依据算法编程的程序,如果程序不满足要求编写程序所耗费的人力物力就浪费了,二是所得时间的统计量依赖于计算机的硬件、软件等环境因素,有时容易掩盖算法本身的优劣,因此人们常常采用另一种事前分析法。

事前分析估算的方法:

一个用户高级程序语言编写的程序在计算机上运行所消耗的时间取决于下列因素

  1. 依据算法先用何种策略,比如循环操作,可使用函数递归或forwhiledo while等循环语句。
  2. 问题的规模,少量数据的问题求解、大规模数量的问题求解、海量数据的问题求解
  3. 书写程序的语言,对于同一种算法,实现的语言级别越高,执行效率就越低。
  4. 编译程序所产生的机器代码的质量。
  5. 机器执行指令的速度,相同的编程语言,使用的编译器不同,产生的机器指令就会不同,从而执行的速度就会不同.

总结:同一个算法用不同的语言实现,或者用不同的编译器进行编译,或者不同的计算机上运行,效率均不相同,这表明使用绝对的时间单位衡量算法的效率是不合适的,抛开这些与计算机硬件、软件有关因素,可以认为特定算法“运行工作量”的大小,只依赖于问题的规模,或者说,它是问题规模的函数。

时间复杂度;

​ 一个算法是由控制结构(顺序、分支、循环等3种基本结构)和原操作(指固有数据类型类型的操作)构成的,则算法时间取决于两者的统合效果,为了便于比较同一问题的不同算法,基本做法是,从算法中选取一种对于 所研究的问题来说是基本操作的原操作,以该基本操作重复执行的次数作为算法的时间量度。因此用算法执行的次数来代表算法的时间复杂度。

​ 所以,算法中基本操作重复执行的次数是问题规模\(n\)的某个函数\(f(n)\),算法的时间量度记叙\(T(n)=O(f(n))\),它表示问题规模\(n\)的增大,算法执行时间的增长率和\(f(n)\)的增长率相同,称做算法的渐近时间复杂度,简称时间复杂度

注意:一般时间复杂度取的是一种大约情况不会精确到特别详细,也就是不会百分百正确。

最差时间复杂度:数据的极端排序顺序会让算法执行效率最差的一种情况。
最优时间复杂度:数据的极端排序顺序会让算法执行效率最优的一种情况。
平均时间复杂度:数据的排序顺序队伍事物发展规律,算法执行效率接近真实的一种情况。

空间复杂度:

​ 它代表随着问题规模\(n\)的某个函数\(f(n)\),它表示问题规模\(n\)的增大,算法所需要的存储空间增长率,被称为算法的空间复杂度
​ 通常来说,只要算法不涉及到动态分配的空间以及递归、栈所需的空间,空间复杂度通常为\(O(1)\)

注意:对于一个算法,其时间复杂度和空间复杂度往往是相互影响的,需要综合考虑

二、查找算法

​ 在一个数据序列中,查找某个数据是否存在或存在的位置,在实际开发过程中使用的频率非常高,例如对数据常见的操作有增、删、改、查,增加数据时需要查询新增加的数据是否重复,删除数据时需要先查询到数据所在位置再删除,修改数据时也需要先查询到被修改的数据在什么位置,查找算法在编程中重要性排列在第一位。

顺序查找:

顺序表的顺序查找

//	从顺序表中从前往后查找数据,找到返回下标,找不到返回-1
int orderSearch(int *arr, size_t len, int key) {
    for (int i = 0; i < len; ++i) {
        if (key == arr[i]) 
            return i;
    }
    return -1;
}

链表的顺序查找

ListNode* orderSearch(ListNode *head, int key) {
    for (ListNode *n = head; NULL != n; n = n->next) {
        if (n->data == key) return n;
    }
    return NULL;
}

顺序查找的优点

  • 对待查找的数据没有有序性的要求,无论是否有序都可以顺序查找
  • 对待查找的数据的存储方式也没有要求,无论是顺序表还是链表都可以顺序查找

顺序查找的缺点:相比于其他查找算法速度要慢,最优时间复杂度\(O(1)\) 最差\(O(N)\) 平均\(O(N)\)

二分查找:

​ 数据序列必须有序,然后关键字key与中间数据比较,如果相等则立即返回,如果key小于中间数据,则只需要在中间数据左边继续查找即可,如果key大于中间数据,则只需要在中间数据右边继续查找即可,重复该步骤,直到找到关键字,或中间数据左右两边为空,则查找失败。

//	循环实现  查找前已有序
int binarySearch(int* arr, size_t len, int key) {
    int l = 0, r = len - 1;
    while(l < r) {
        int mid = l + r >> 1;
        if (arr[mid] == key) {
            return p;
        }
        if (arr[mid] < arr[r]) {
            r = mid;
        } else {
            l = mid + 1;
        }
    }
    return -1;
}

//	递归实现
int _binarySearch(int *arr,size_t len, int key, int l, int r) {
    if (l > r) return -1;
    int mid = l + r >> 1;
    if (key < arr[mid]) {
        return _binarySearch(arr, len, key, l, mid - 1);
    } else if (key > arr[p]) {
        return _binarySearch(arr, len, key, mid + 1, r);
    }
    return p;
}

int binarySearch(int* arr, size_t len, int key) {
    return _binarySearch(arr, len, key,0, len - 1);
}

二分查找的优点:​查询速度快,时间复杂度:\(O(log2N)\)
二分查找的缺点

  • 对待查找的数据有序性有要求,必须先排序才能二分查找
  • 对数据存储结构有要求,不适合链式表中直接使用,因为无法快速地访问中间位置,以及快速地让左右标杆位置进行移动

索引查找:

​ 索引查找是在索引表和主表(即线性表的索引存储结构)上进行的查找,但需要先建立索引表,索引表就类似图书的目录,能大提高查找效率。

给顺序表创建索引表:
typedef struct Stduent {
    int id;
    char name[20];
    char sex;
   	short age;
    float score;
} Student;

Student stu[10000];	//	主表

//	索引表元素结构
typedef struct Index {
    int id;
    void* addr;
} Index;

//	索引表
Index indexs[10000];

//	对主表与索引表之间建立索引
void create_index(Student* stu, Index* indexs, size_t len) {
    for(int i=0; i<len; ++i) {
        indexs[i].id = stu[i].id;
        indexs[i].addr = stu+i;
    }
}
//	注意:建立索引表后,后面使用索引查找的是索引表,再通过找到的索引表中记录的主表元素的位置信息,来最终找到待查找的数据元素
//	注意:索引表如何建立索引,根据实际需求来选择
索引表的顺序查找:
//	它比直接查询数据的主表的跳转速度要快,如果使用stu[i]查找,i每加1,要跳转过sizeof(Student)个字节数,如果使用索引表indexs[i]查询,i每加1,则只需要跳转sizeof(Index)个字节数,比主表跳转字节数要少。
//	如果主表的数据不存在内存而是存储在磁盘上,而索引表是建立在内存中,通过索引表访问效率、速度远高于访问磁盘的主表
int orderIndexSearch(Index *indexs, size_t len, int id) {
    for (int i = 0; i < len; ++i) {
        if (indexs[i].id == id)
            return i;
    }
   	/*
   	for(int i=0; i<len; i++)
   	{
   		if(stu[i].id == id)
        	return i;
   	}
   	*/
}
索引表二分查找:
//	需要对索引表先排序
//	对索引表排序的效率和速度要远比直接对主表的排序要快
void sortIndex(Index* indexs, size_t len) {
    for (int i = 0; i < len - 1; ++i) {
        int min = i;
        for (int j = i + 1; j < len; ++j) {
            if (indexs[j].id < indexs[min].id)
                min = j;
        }
        if (min != i) {
            Index temp = indexs[min];
            indexs[min] = indexs[i];
            indexs[i] = temp;
        }
    }
}

//	对索引表进行二分查找
//	因为索引表已经重新排序了,而主表没有排序过,所以不能返回在索引表中找到元素的下标,该下标与主表对应元素的下标很可能不一致了,所以需要直接返回主表对应元素的地址
Student* binaryIndexSearch(Index *indexs, size_t len, int id) {
    int l = 0, r = len - 1;
    while (l <= r) {
        int mid = l + r >> 1;
        if (indexs[p].id == id) {
            return indexs[p].addr;
        } else if (id < indexs[mid].id) {
            r = mid;
        } else {
            l = mid + 1;
        }
    }
    return NULL;
}
给链表创建索引表:
//	给链表head创建一张顺序的索引表 表中的元素是ListNode* 用来指向链表中的节点 
//	返回值是返回索引表首地址,len_i输出型参数,返回索引表中元素的个数
ListNode** createIndexList(ListNode *head, size_t *len_i) {
    if(NULL == head || NULL == len_i) return NULL;
    //	索引表的长度
    *len_i = 0;
    ListNode** indexs = NULL;
    //	遍历链表head 给每个节点建立普通索引
    for (ListNode* n = head; NULL != n; n = n->next) {
        //	申请索引表元素ListNode*的内存
        indexs = realloc(indexs,sizeof(ListNode*)*(*len_i + 1));
        //	让索引表中的最后一个元素指向对应的链表节点
        indexs[(*len_i)++] = n;
    }
    //	对索引表进行排序,交换索引表中指针的指向
    for (int i = 0; i < (*len_i) - 1; ++i) {
        int min = i;
        for (int j = i + 1; j < *len_i; ++j) {
            if (indexs[j]->data < indexs[min]->data)
            	min = j;
        }
        if (min != i) {
            //	交换索引表中指针的指向 不修改链表
            ListNode* temp = indexs[min];
            indexs[min] = indexs[i];
            indexs[i] = temp;
        }
    }
}

//	链表的二分查找,本质上是对顺序的索引表进行二分
int binaryListIndexSearch(ListNode** indexs, size_t len, int key) {
    int l = 0, r = len - 1;
    while(l < r) {
        int mid = l + r >> 1;
        if (indexs[mid]->data == key) {
            return p;
        } else if (key < indexs[mid]) {
            r = mid;
        } else { 
            l = mid + 1;
        }
    }
    return -1;
}

索引查找的优点

  • 对于顺序表的顺序查找,索引查找可以缩短数据的查找跳转范围
  • 对于顺序表的二分查找,通过排序索引表也能提高排序的速度
  • 对于链式表,可以先建立顺序的索引表后,进行之前无法实现的二分查找了

索引查找的缺点:使用了额外的存储空间来创建索引表,是一种典型的以空间换时间的算法策略

索引查找的使用场景

  • 如果针对的是内存中的顺序表中的数据,通过索引查找提升的速度和效率其实并不是很明显,毕竟内存的运算速度很快
  • 但是对于存储在机械硬盘上的数据,通过在内存中建立对应硬盘数据的索引表,访问起来提升的效率就很多了,因此在一些常用的数据库中的索引查找使用非常多

分块查找:

​ 先对数据进行分块,可以根据日期进行分块,可以对整形数据根据数据的最后一位分为10个分表,针对字符串数据可以根据第一个字母分为26个分表,然后再对这些分表进行二分查找、顺序查找,它是分治思想的具体实现。

分块查找的优点:可以把海量数据进行分化,降低数据规模,从而提升查找速度。
分块查找的缺点:​操作比较麻烦,可能会有一定的内存浪费。

二叉排序树和平衡二叉树:

​ 二叉排序树也叫二叉搜索树是根据数据的值进行构建的,然后进行前序遍历查找,它的查找原理还是二分查找,我在二叉搜索树中已经详细过,在此不再赘述。

​ 平衡二叉树首先是一棵二叉排序树或二叉搜索树,二叉排序树在创建时,由于数据基本有序,会导致创建出的二叉排序树呈单枝状,或者随机数据的插入、删除导致二叉排序树左右失衡呈单枝状,就会导致二叉树排序的查询速度接近单向链表的\(O(N)\)

​ 平衡二叉树要求左右子树的高度差不超过1,这样就会让二叉排序树左右均匀,让二叉排序树的查找速度接近二分查找(要了解AVL树和红黑树的区别)。

哈希表查找:Hash

​ 在查找数据时需要进行一系列的比较运算,无论是顺序查找、二分查找、索引查找、这类的查找算法都是建立在比较的基础上的,但最理想的查找算法是不经过任何比较,一次存取就能查找到数据,那么就需要在存储位置和它的关键字之间建立一个确定的对应关系,使每个关键字和数据中的唯一的存储位置相对应。在查找时,根据对应关系找到给定的关键字所对应的数据,这样可以不经过比较可以直接取得所查找的数据。

​ 我们称存储位置在关键字之间的对应关系为哈希函数,按这个思想建立的表为哈希表。

直接定值法:

取关键字的值或某个线性函数作为\(k\)哈希地址:\(Hash(key) = key\)\(Hash(key)=a*key-b\),但这种方法对数据有很高的要求。

时间复杂度\(O(1)\)

​ 但是有很大的局限性,因为很多数据是无法直接用做数组的下标的,其次可能会出现数据量少,但是数据值的差值较大,导致哈希表长度过大,造成内存浪费

数字分析法:

分析数据的特点设计哈希,常用方法是找到最大最小值,最大值-最小值+1 确定哈希表的长度,通过 数据-最小值 访问哈希表下标

以上两种方法局限比较大,但计算出的哈希地址没有冲突的可能,以下方法:平方取中法、折叠法、除留余数法等方法对关键字的要求不要,但计算出的哈希地址可能会有冲突。称为哈希冲突

解决哈希值冲突的方法:
  • 开方地址法
  • 再哈希法
  • 链地址法
  • 创建公共溢出区

哈希查找的优点:​查找速度极快,时间复杂度能达到\(O(1)\)
哈希查找的缺点

  1. 局限性比较大,对数据的要求比较高。
  2. 设计哈希函数麻烦,没具体的设计哈希函数的方法,只是有一些大致的思路。
  3. 需要建立哈希表,占用了额外的空间。

Hash函数的应用:MD5、SHA-1都属于Hash算法

三、排序算法

排序算法介绍:

​ 排序的功能是将数据元素的任意序列,重新排列成按照关键字有序的序列

​ 它之所以重要是因为查找操作很重要,而有序的顺序表可以采用效率较高的二分查找\(O(logN)\),而无序的查找只能顺序查找\(O(N)\), 而创建的二叉搜索树、平衡二叉树、堆的过程其实就是一个排序过程。

排序算法的稳定性:

​ 假设待排序的序列中可能存在两个或两个以上关键字相同的数据,k[i] == k[j] && i < j,在排序的整个过程中,i必定在j的前面,那么认为该排序算法是稳定的,反之认为是不稳定的排序。(稳定性是指相等的元素经过排序之后相对顺序是否发生了改变。)

基数排序、计数排序、插入排序、冒泡排序、归并排序是稳定排序。
选择排序、堆排序、快速排序、希尔排序不是稳定排序。

1、经典排序:

​ 仅能够完成排序操作,但没有任何的优化动作,没有进行数据比较次数减少的优化,数据交换的次数也没有优化,没有任何优点,不能称得上是算法。

void classicSort(TYPE *arr, size_t n) {
    printf("%s: ", __func__);
    for (int i = 0; i < n - 1; ++i) {
        for (int j = i + 1; j < n; ++j) {
            if (arr[j] < arr[i]) {
                SWAP(arr[i], arr[j]);
            }
        }
    }
}
2、冒泡排序(稳定)

​ 排序的过程类似水中气泡上升,越大的气泡上升越快。

​ 从第一个数据开始,让前后相邻的数据两两比较,如果k[i] > k[i + 1]则交换它们,每一趟冒泡排序完成一个数据的排序,反复以上过程,直到待排序的数据为1,结束排序

​ 与其他排序相比,冒泡排序对数据的有序性敏感,如果在一趟的排序过程中,没有发生一次交换,则说所有数据前后两两有序,则可以立即停止冒泡排序,提前结束。

注意如果待排序的序列基本有序,则使用冒泡排序速度最快,因为可以提前结束

void bubbleSort(TYPE *arr, size_t n) { // 冒泡排序
    printf("%s: ", __func__);
    bool suc = true;
    for (int i = n - 1; i > 0 && suc; --i) {
        suc = false;
        for (int j = 0; j < i; ++j) {
            if (arr[j] > arr[j + 1]) {
                SWAP(arr[j + 1], arr[j]);
                suc = true;
            }
        }
    }
}
3、选择排序(不稳定)

是对经典排序的一种优化,在待排序数据i后面找最小值的下标,如果有比min更小的数据,则先更新min下标,当后面所有数据都比较完后,如果min != i,则交换mini的值,继续重复以上步骤。

和经典排序相比,数据的比较次数没有减少,但是数据交换次数大大降低\(O(N-1)\),节约了很多的数据交换时间,所以虽然时间复杂度没变化\(O(N^2)\),但是它的排序效率比经典排序提高很多

注意选择排序最突出的特点是数据交换次数最少的,如果待排序的元素的字节数较多,比如:结构、类对象时,则此时使用选择排序速度最快

void selectionSort(TYPE *arr, size_t n) { // 选择排序
    printf("%s: ", __func__);
    for (int i = 0; i < n - 1; ++i) {
        int k = i;
        for (int j = i + 1; j < n; ++j) {
            if (arr[k] > arr[j]) {
                k = j;
            }
        }
        if (k != i) {
            SWAP(arr[k], arr[i]);
        }
    }
}
4、插入排序(稳定)

往有序的序列中添加新的数据,使得序列保持有序。
步骤:假定新的数据位置是i,值是val,让该数据与前面的数据从后往前逐一比较,如果val < arr[i - 1],则把前面的数据往后一位拷贝,然后i--,然后重复以上操作,直到val不再小于前面的值,或者i走完所有数据,则该位置就是val应该存放的位置,把数据val放入则完成本次插入操作

使用以上步骤,对无序的序列,看成左右两部分,左边是已经有序的,和右边带插入的无序序列,逐一往左边部分完成插入操作,当无序部分插入完成即完成排序操作。

注意插入排序适合往已经有序的序列中添加新的数据,优点是排序过程中没有数据交换,节约了大量的无效的时间

void insertionSort(TYPE *arr, size_t n) { // 插入排序
    printf("%s: ", __func__);
    for (int i = 1; i < n; ++i) {
        TYPE key = arr[i];
        int j = i - 1;
        while (j >= 0 && arr[j] > key) {
            arr[j + 1] = arr[j];
            j--;
        }
        arr[j + 1] = key;
    }
}
5、希尔排序(不稳定)

​ 设计该算法的作者叫希尔,所以叫希尔排序,在插入排序的基础上引入增量的概念(数据在插入的过程中,每次移动的距离),原来的插入排序每次只移动数据一个位置,当数据量比较大,或者数据距离最终正确位置较远时,原来的插入排序速度较慢,在希尔排序中最开始以序列长度的一半作为移动增量,进行第一次插入排序,然后再对半减少移动增量,直到增量为1时,每个数据已经很接近最终位置,最后再完成一次插入排序,就完成排序。

注意希尔排序适合在数据量大的时候,或者非常无序的序列中添加新的数据。

void shellSort(TYPE *arr, size_t n) { // 希尔排序
    printf("%s: ", __func__);
    int h = 1;
    while (h < n / 3) {
        h = 3 * h + 1;
    }
    while (h >= 1) {
        for (int i = h; i < n; ++i) {
            for (int j = i; j >= h && arr[j] < arr[j - h]; j -= h) {
                SWAP(arr[j], arr[j - h]);
            }
        }
        h = h / 3;
    }
}
6、快速排序(不稳定)

​ 先找到一个标杆位置p,一般是待排序序列的最左边(也可以任意,中间、最右边),然后备份标杆的值val,然后找到待排序序列的最左标标杆l,和最右标杆r,然后先让r往左走,找比val小的数,找到了把该值赋值给p位置,然后更新pr;当pr重合后,换成l从左往右找比val更大数,找到了把该值赋值给p位置,然后更新pl,重复以上过程,最终lr相遇时,结束这一次快排,把val的值赋值到相遇点p,结束后,val的左边都比它小,右边都比它大,达到局部有序,然后继续对左右两边分别进行同样快排,最后全部达到有序停止。

注意快速排序之所以成为快排,综合各种情况下它的表现是最好、速度最快的,所以如果对于待排序的数据不了解时,建议优先选择快排

void _quickSort(TYPE *arr, int l, int r) {
    if (l >= r) return ;
    int i = l - 1, j = r + 1, x = arr[l + r >> 1];
    while (i < j) {
        do i++; while (x > arr[i]);
        do j--; while (x < arr[j]);
        if (i < j) SWAP(arr[i], arr[j]);
    }
    _quickSort(arr, l, j), _quickSort(arr, j + 1, r);
}

void quickSort(TYPE *arr, size_t n) { // 快速排序
    printf("%s: ", __func__);
    _quickSort(arr, 0, n - 1);
}

7、堆排序(不稳定)

​ 堆是一种特殊的二叉树,有两种堆,大顶堆(根节点的值大于左右子树,并且所有子树都满足),小顶堆(根节点的值小于左右子树,并且所有子树都满足)

​ 所谓的堆排序就是把一个数据当做大顶堆\小顶堆处理,逐步把堆顶的最值交换到序列的末尾,然后重新调整剩下的数据变回堆结构,重复最终有序。

注意理论上堆排序的速度并不比快排慢,但是对于无序的序列需要先构建成堆结构,时间复杂度已经需要\(O(N)\),然后再逐一堆顶出堆完成堆排序\(O(NlogN)\),因此无序序列的堆排序不见得很快,所以实际应用中不会使用堆排序

void _siftSort(TYPE *arr, int start, int end) {
    int root = start;
    int son = start << 1;
    while (son <= end) {
        if (son + 1 <= end && arr[son - 1] < arr[son]) {
            son += 1;
        }
        if (arr[root - 1] < arr[son - 1]) {
            SWAP(arr[root - 1], arr[son - 1]);
            root = son;
            son <<= 1;
        } else return ;
    }
}

void heapSort(TYPE *arr, size_t n) { // 堆排序
    printf("%s: ", __func__);
    for (int i = n / 2; i >= 1; --i) {
        _siftSort(arr, i, n);
    }
    for (int i = n; i > 1; --i) {
        SWAP(arr[0], arr[i - 1]);
        _siftSort(arr, 1, i - 1);
    }
}
8、归并排序(稳定)

​ 先把待排序的序列以k = 2为单位进行分组拆分,每组有左右两部分,按照从小到大的顺序合并到另一个内存空间的对应位置,然后让k *= 2,继续两组两组合并,最终当k / 2 >= len则排序完成,在合并的过程,合并的左右部分,都是有序的

​ 归并排序需要一块额外的内存空间,用于存储每次合并的结果,因此节约了大量的数据交换的时间,但是也耗费了额外的空间,也是以空间换时间的策略

注意如果用户对排序的速度有高要求,但是又不在意内存的消耗,适合使用归并排序

void _mergeSort(TYPE *arr, TYPE *tmp, int l, int r) {
    if (l >= r) return ;
    int mid = l + r >> 1;
    _mergeSort(arr, tmp, l, mid);    
    _mergeSort(arr, tmp, mid + 1, r); 
    int a = l, b = mid + 1;
    for (int i = l; i <= r; ++i) {
        if (b > r || (a <= mid && arr[a] <= arr[b])) {
            tmp[i] = arr[a++];
        } else {
            tmp[i] = arr[b++];
        }
    }
    memcpy(arr + l, tmp + l, sizeof(TYPE) * (r - l + 1));
}

void mergeSort(TYPE *arr, size_t n) { // 归并排序
    printf("%s: ", __func__);
    TYPE *temp = malloc(sizeof(TYPE) * n);
    _mergeSort(arr, temp, 0, n - 1);
}
9、计数排序(稳定)

​ 找出数据中的最大值和最小值,创建哈希表,把 数据-最小值 当做访问哈希表的下标去给对应位置计数+1,然后遍历所有数据对哈希表进行计数

​ 然后遍历哈希表,当表中的数据大于0时,再通过该位置的下标+最小值得到原数据,并依次放回原数组中,得到有序。

注意理论上该算法速度非常快,它不是基于比较的算法,在一定范围的正整数数据的排序中,要快于任何一种比较的排序算法,但是有很大的局限性,只适合整形数据,并且数据的差值不宜过大,否则非常浪费内存,因此数据越平均,重复数越多,性价比越高

void countingSort(TYPE *arr, size_t n) {  // 计数排序
    printf("%s: ", __func__);
    TYPE min = arr[0], max = arr[0];
    for (int i = 1; i < n; ++i) {
        min = min > arr[i] ? arr[i] : min;
        max = max < arr[i] ? arr[i] : max;
    }
    TYPE* hash = calloc(sizeof(TYPE), max - min + 1);
    for (int i = 0; i < n; ++i) {
        hash[arr[i] - min] += 1;
    }
    for (int i = 0, j = 0; i < max - min + 1; ++i) {
        while (hash[i]--) {
            arr[j++] = i + min;
        }
    }
    free(hash);
}
10、基数排序(稳定)

​ 先创建10个队列并编号0~9,然后逆序计算出每个数据的个、十、百...的数,然后入队到对应编号的队列中,然后依次把每个队列的值出队,再计算下一位的值并入队,依次循环,直到最大值的所有位数都处理过后,最后一次出队的顺序就是排序成功后的序列

​ 使用该排序也需要额外的内存空间(队列),也是以空间换时间策略

注意数据的位数不多,差别不大的整型数据才适合

void radixSort(TYPE *arr, size_t n) { // 基数排序
    printf("%s: ", __func__);
    int *brr = calloc(sizeof(TYPE), n);
    int *cnt = calloc(sizeof(TYPE), 1 << 8);
    int mask = (1 << 8) - 1;
    int *x = arr, *y = brr;
    for (int i = 0; i < 32; i += 8) {
        memset(cnt, 0, sizeof cnt);
        for (int j = 0; j < n; ++j) {
            ++cnt[x[j] >> i & mask];
        }
        for (int sum = 0, j = 0; j < (1 << 8); ++j) {
            sum += cnt[j];
            cnt[j] = sum - cnt[j];
        }
        for (int j = 0; j < n; ++j) {
            y[cnt[x[j] >> i & mask]++] = x[j];
        }
        SWAP(x, y);
    }
    free(brr);
    free(cnt);
}
11、桶排序

​ 桶排序是把待排序的数据,一般根据值的不同范围,划分到不同的“桶”中,然后再根据桶中数据的特点,选择合适的其他的排序算法对各个桶中的数据分别进行排序,最终合并桶中的数据达到了排序的目的

​ 之所以使用桶排序的思想,是因为待排序的数据量非常多的时候,会影响排序算法的性能,桶排序的目的是对数据分类后,降低了数据的规模,并且可能按照特征分类,从而提高排序的效率,和选择更合适的排序算法

// 分桶排序 cnt桶数 range桶中数据范围
void _bucketSort(TYPE *arr, size_t n, int cnt, TYPE range) {
    // 申请桶内存
    // bucket指向桶的开头 end指向桶的末尾 数据加入到end的位置
    TYPE *bucket[cnt], *end[cnt];
    for (int i = 0; i < cnt; ++i) {
        bucket[i] = malloc(sizeof(TYPE) * n);
        end[i] = bucket[i];
    }
    for (int i = 0; i < n; ++i) {
        for (int j = 0; j < cnt; ++j) {
            if (j * range <= arr[i] && arr[i] <= range*(j + 1)) {
                *(end[j]) = arr[i];
                end[j]++;
            }
        }
    }
    // 对每桶分别使用其他排序算法来排序
    for (int i = 0; i < cnt; ++i) {
        int size = end[i] - bucket[i];
        if (size > 1) {
            _quickSort(bucket[i], 0, size - 1);
        }
        memcpy(arr, bucket[i], sizeof(TYPE) * size);
        arr += size;
        free(bucket[i]);
    }
}

// 桶排序 只对正整数
void bucketSort(TYPE *arr, size_t n) {
    printf("%s: ", __func__);
    _bucketSort(arr, n, 4, 25);
}

排序算法的完整代码

posted @ 2024-07-28 17:44  sleeeeeping  阅读(20)  评论(0编辑  收藏  举报