1,概念
在最好情况下,直接插入排序、冒泡排序的 时间复杂度最低。
在评价情况下,直接快速排序、堆排序、归并排序的 时间复杂度最低。
1)插入排序和选择排序
插入排序:直接插入排序、折半插入排序、2-路插入排序、表插入排序、希尔排序、快速排序、冒泡排序
选择排序:简单选择排序、堆排序
2)内排序和外排序
内排序:指在排序期间数据对象全部存放在内存的排序。分为插入排序、选择排序、交换排序、归并排序及基数排序等几大类。
外排序:指在排序期间全部对象太多,不能同时存放在内存中,必须根据排序过程的要求,不断在内,外存间移动的排序。外排序用读/写外存的次数来衡量其效率。
归并排序在外排序中用的也挺多。
3)原地排序
指不申请多余空间排序,松一点的说法是可以用很小的固定的辅助空间。
2, 直接插入排序(Insertion Sort)(重要)
1)原理
每次将一个待排序的记录,按其关键字大小插入到前面已经排好序的子序列中的适当位置,直到全部记录插入完成为止。
某一趟结束后未必能选出一个元素放在其最终位置上。
2)代码实现
public static void insertSort(int[] a){
for(int i=1; i<a.length; i++){//只有一个数直接插入,所以i不是从0开始
int temp = a[i];//待插入元素
int j;
for (j=i-1; j>=0; j--){//将大于temp的往后移动一位
if (temp < a[j]){
a[j+1] = a[j];
}else{
break;
}
}
a[j+1] = temp;//插入进来
}
}
3)稳定性
稳定。
4)复杂度
①空间复杂度:O(1)。
②时间复杂度:
最好的情况:原来本身有序,比较次数为n-1次时间复杂度为O(n)
最坏的情况:原来为逆序,比较次数为1 + 2 + 3 + … + (N - 1)次 (O(n^2)),而记录的移动次数为1 + 2 + 3 + … + (N - 1)次 (O(n^2)),时间复杂度为 O(n^2)。
5)举例
3, 折半插入排序
在折半查找的基础上进行的插入排序。
1)原理
①每次插入,都从前面的有序子表中查找出待插入元素应该被插入的位置; (折半查找)
②给插入位置腾出空间,将待插入元素复制到表中的插入位置。
2)代码实现
public static void insertSortBinary(int[] a){
int low, high, mid;
int temp;//待插入的数
for (int i=1; i<a.length; i++){
temp = a[i];
//折半查找范围
low = 0;
high = i-1;
while (low <= high){
mid = (low+high) / 2;
if (a[mid] > temp){
high = mid-1;//查找左半元素
}else{
low = mid+1;
}
}
for(int j=i-1; j>= high + 1; j--){
a[j+1] = a[j];//后移空出插入位置
}
a[high + 1] = temp;//插入
}
OutputMessage.showMessage(a);
}
3)稳定性
稳定的。
4)复杂度
①时间复杂度 :O(n²)
折半插入排序仅仅是减少了比较元素的次数,约为O(nlogn),而且该比较次数与待排序表的初始状态无关,仅取决于表中的元素个数n;而元素的移动次数没有改变,它依赖于待排序表的初始状态。因此,折半插入排序的时间复杂度仍然为O(n²),但它的效果还是比直接插入排序要好。
②空间复杂度:O(1)。
稳定性
折半插入排序是一种稳定的排序算法。
4, 2-路插入排序
1)原理
折半插入排序的改进,通过增加n个记录的辅助空间,减少排序过程中移动记录的次数。
①开辟一个等长的临时数组(循环数组),将待排序数组的第1个元素放到临时数组的第0位,作为初始化。将该值作为每次排序的参照,大于等于这个参照值就后插,小于参照值就前插。
②同时定义两个游标first和final分别指向临时数组当前最小值和最大值所在位置。
2)代码实现
public static void TwoInsertSort(int[] a){
int first=0, finals = 0;
int length = a.length;
int []b = new int[length];//辅助空间
b[0] = a[0];
for(int i = 1; i < length; i++){
if(a[i] > b[finals]){
finals++;
b[finals] = a[i];
}
if(a[i] < b[first]){
first=(first-1+length)%length;
b[first]=a[i];
}
if(a[i] > b[first] && a[i]< b[finals]){//用循环数组理解
int j = finals;
finals++;
while(a[i] <= b[j]){
b[(j+1)%length] = b[j];
j = (j-1+length)%length;
}
b[(j+1)%length] = a[i];若j停留在数组最后的位置(j+1)%length
}
OutputMessage.showMessage(b);
System.out.print("finals=" + finals + ",first=" + first);
}
/*
* 将数组重新复制到a[]数组中(调整循环数组顺序)
*/
for(int i = 0; i < length;i++){
a[i] = b[(first++)%length];
}
OutputMessage.showMessage(a);
}
3)稳定性
稳定
4)复杂度
和折半复杂度一致,但避免了数据的移动(不是绝对的避免)。
5)举例
5, 表插入排序
1)原理
将一个记录插入到已排好序的有序链表中,通过修改指针的值来代替移动记录。
利用静态链表的形式,分两部完成:
①对一个有序的循环链表,插入一新的元素,修改每个节点的后继指针的指向,使顺着这个指针的指向,元素是有序的。在这个过程中,我们不移动或交换元素,只是修改指针的指向。
②顺着指针的指向调整元素的位置,使其在链表中真正做到物理有序。
2)代码实现
/**
* 表插入排序,相当于静态链表
* 从小到大排序
*/
public static void table_insertion_sort(int[] a) {
TableSortEntity[] arr = getTableSortArr(a);
int current, pre;//current当前节点 pre前一个节点
for (int i = 2; i < a.length + 1; i++) { //修改next域,使其按指针指向有序
pre = 0;
current = arr[pre].getNext();
while (arr[i].getData() >= arr[current].getData()) { // >= 保证了排序的稳定性
pre = current;
current = arr[pre].getNext();
}
arr[i].setNext(current);
arr[pre].setNext(i);
}
current = arr[0].getNext();
int i = 0;
while (current != 0) { //顺着静态链表的指针指向,回写数据到原数组
a[i++] = arr[current].getData();
current = arr[current].getNext();
}
array_table(arr);
}
private static TableSortEntity[] getTableSortArr(int[] a) {
int MAX = 20;
TableSortEntity[] arr = new TableSortEntity[a.length + 1];//arr[0]为头结点,为循环终止创造条件,头结点值域MAX应大于原序列中的最大值。
for (int i = 0; i < a.length; i++) { //把数据赋给链表
TableSortEntity tableSortEntity = new TableSortEntity();
tableSortEntity.setData(a[i]);
tableSortEntity.setNext(0);
arr[i + 1] = tableSortEntity;
}
TableSortEntity tableSortEntity = new TableSortEntity();
tableSortEntity.setData(MAX);
tableSortEntity.setNext(1);//头节点和第一个节点构成了循环链表
arr[0] = tableSortEntity;
return arr;
}
private static void array_table(TableSortEntity[] arr) {
int q, p = arr[0].getNext();
for (int i = 1; i < arr.length; i++) {
while (p < i)
p = arr[p].getNext();
q = arr[p].getNext(); // q记录下一次待归位节点的下标
if (p != i) { //如果p与i相等,则表明已在正确的位置上,那就不需要调整了
TableSortEntity temp = arr[i];
arr[i] = arr[p];
arr[p] = temp;
arr[i].setNext(p);
}
p = q;
}
System.out.println("排序结果:");
for (int j = 1; j < arr.length; j++) {
System.out.print(arr[j].getData() + ",");
}
}
public class TableSortEntity {
int data; //值域
int next; //静态链表的链域
}
3)稳定性
稳定
4)复杂度
时间复杂度:O(n2),避免了移动。
5)举例
6, 分段插入排序
1)原理
①已知一组升序排列数据a[1]、a[2]、……a[n],一组无序数据b[1]、b[2]、……b[m],需将二者合并成一个升序数列。先将数组a分成x等份(x<<n)
,每等份有n/x个数据。将每一段的第一个数据先储存在数组c中:c[1]、c[2]、……c[x]。
②运用插入排序处理数组b中的数据。插入时b先与c比较,确定了b在a中的哪一段之后,再到a中相应的段中插入b。随着数据的插入,a中每一段的长度会有变化,所以在每次插入后,都要检测一下每段数据的量的标准差s,当其大于某一值时,将a重新分段。在数据量特别巨大时,可在a中的每一段中分子段,b先和主段的首数据比较,再和子段的首数据比较,可提高速度。
2)优缺点
优点:快,比较次数少;
缺点:不适用于较少数据的排序,s的临界值无法确切获知,只能凭经验取。
7, 希尔排序(Shell’s Sort,缩小增量排序,Diminishing Increment Sort)(重要)
1)原理
希尔排序是把记录按下标的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至1时,整个文件恰被分成一组,算法便终止。
因为希尔排序 是缩小增量排序,所以对于初始序列有序还是无序没有直接关系。只与增量序列选择有关。
2)代码实现
/**
* 希尔排序 针对有序序列在插入时采用移动法
* @param arr
*/
public static void shellSort(int[] arr) {
//增量gap,并逐步缩小增量
for (int gap = arr.length / 2; gap > 0; gap /= 2) {
//从第gap个元素,逐个对其所在组进行直接插入排序操作
for (int i = gap; i < arr.length; i++) {
int j = i;
int temp = arr[j];
if (arr[j] < arr[j - gap]) {
while (j - gap >= 0 && temp < arr[j - gap]) {
//移动法
arr[j] = arr[j - gap];
j -= gap;
}
arr[j] = temp;
}
}
}
}
3)稳定性
不稳定。
4)复杂度
①空间复杂度:O(1)。
②时间复杂度:
最坏时间复杂度依然为O(n2),一些经过优化的增量序列如Hibbard经过复杂证明可使得最坏时间复杂度为O(n3/2).
Shell排序比冒泡排序快5倍,比插入排序大致快2倍。Shell排序比起QuickSort,MergeSort,HeapSort慢很多。但是它相对比较简单,它适合于数据量在5000以下并且速度并不是特别重要的场合。它对于数据量较小的数列重复排序是非常好的。
5)举例
如下图所示。
首先,步长k= n/2 = 10/2 = 5,则R1和R6分为一组,依次类推,共5个子序列:{R1,R6},{R2,R7},{R3,R8},{R4,R9},{R5,R10}.
在每一对子序列内,从小到大排序,然后合并结果为第一趟排序结果。
第二趟排序,k = k/2 = 5/2 = 2。以此类推,直到k=1停止。
8, 快速排序(重要)
1)原理
整体思想:
①先从数列中取出一个数作为基准数。
②分区过程,将比这个数大的数全放到它的右边,小于或等于它的数全放到它的左边。
③再对左右区间重复第二步,直到各区间只有一个数。
一次划分:
①设置2个变量i=0、j=N-1;
②第一个数为基准key;
③j向前搜索,j–,找到第一个比key小的数A[j],将A[i]与A[j]交换;
④i向后搜索,i++,找到第一个比key大的数A[i],将A[i]与A[j]交换;
⑤重复第三、四步,直到i>=j。
2)代码实现
注意:
函数Partition除了可以用在快速排序算法中,还可以用来实现在长度为n的数组中查找第k大的数字。
package QuickSort;
/**
* 快速排序(从大到小)
* @author luo
* 分析:
* 1,分解:以a[p]为基准将啊[p:r]划分为3段:a[p:q-1](小于基准元素),a[q](等于基准元素即就是a[p]),a[q+1:r](大于基准)
* 2,递归求解
* 3,合并
*/
public class QuickSort {
/**
* 递归算法
* @param a
* @param p 起始下标
* @param r 结束下标
*/
public static void quickSort(int[] a,int p,int r){
if (p < r){
int q = Partition(a,p,r);
quickSort(a,p,q-1);
quickSort(a,q+1,r);
}
}
/**
* 划分元素
* @return
*/
public static int partition(int a[], int s, int e) {
int pivot = a[s];//基准
while (s < e) {
while (s < e && a[e] >= pivot) e--;//找出比x大的数
a[s] = a[e];//这样分步交换可以节省一个存储空间
while (s < e && a[s] <= pivot) s++;//找出比x小的数
a[e] = a[s];
}
a[s] = pivot;
return s;
}
}
3)稳定性
不稳定。(在快速排序的随机选择比较子阶段)
4)复杂度
①空间复杂度:O(logn)。
②时间复杂度:
快速排序的运行时间和划分是否对称有关。
最坏情况:
划分的2个区域包含n-1个和1个元素。(待排序数列已基本有序)
T(n) = T(n-1) + O(n) = O(n^2)
最好情况:
划分对称,2个区域都为n/2个元素。
T(n) = 2T(n/2) + O(n) = O(nlogn)
9, 冒泡排序(Bubble Sort,泡沫排序或气泡排序)(重要)
1)原理
它重复地走访过要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。
2)代码实现
改进(增加交换标记)
public static void bubbleSort(int[] a){
boolean isSwap = false;//交换标准。未发生交换为false
for (int i = 0; i < a.length; i++) {
isSwap = false;
for(int j = 0; j< a.length - 1 - i; j++){
if (a[j] > a[j+1]) {
Swap.Swap(a, j, j+1);
isSwap = true;
}
}
if(!isSwap){
break;
}
}
}
3)稳定性
稳定。
4)复杂度
①空间复杂度:O(1)。
②时间复杂度:
最好的情况,n-1次比较,移动次数为0,时间复杂度为O(n)。
最坏的情况,n(n-1)/2次比较,等数量级的移动,时间复杂度为O(O^2)。
平均时间复杂度:O(n^2)
10, 简单选择排序(重要)
1)原理
设所排序序列的记录个数为n。i取1,2,…,n-1,从所有n-i+1个记录(Ri,Ri+1,…,Rn)中找出排序码最小的记录,与第i个记录交换。执行n-1趟 后就完成了记录序列的排序。
关键字比较的次数与记录的初始排列次序无关.
2)代码实现
/**
* 简单选择排序
* 原理:从i到args.length-1,每次迭代将i到args.length-1中最小(最大)的那个数交换到i的位置,然后i++,再循环
* @param array 待排序的数组
*/
public static void simpleSelectMethod(int[] array) {
//minLoc用于记录i+1到args.length-1这个区间的最小值的下标(i会递增),i表示要交换的位置。
for (int i = 0, j = 0, minLoc = 0; i < array.length; i++) {
minLoc = i;
for (j = i + 1; j < array.length; j++) {//找出i+1到args.length-1这个区间的最小值的下标
if (array[j] < array[minLoc]) {
minLoc = j;
}
}
if (minLoc != i) {//如果minLoc!=i,说明minLoc有变化,就进行交换
swap(array, i, minLoc);
}
}
}
3)稳定性
不稳定。
4)复杂度
①空间复杂度:O(1)。
②时间复杂度:
最坏情况:比较次数为 n(n-1)/2,交换次数为n-1;
最好情况:比较次数为 n(n-1)/2,交换次数为0。
平均:O(n^2)
③新建堆的复杂度:O(n)
11,堆排序 (重要)
1)原理
①R[1…n]构建大根堆,此堆为初始的无序区 。
②将关键字最大的记录R[1](即堆顶)和无序区的最后一个记录R[n]交换,由此得到新的无序区R[1…n-1]和有序区R[n],且满足R[1…n-1].keys≤R[n].key
③由于步骤2的的交换可能破环了大根堆的性质,故应将当前无序区R[1…n-1]调整为堆(重复步骤2)。
是对树型选择排序占用空间多的改进。
堆的定义
定义一:
小(大)根堆:完全二叉树中任意结点小于(大于)其孩子。
定义二:
n个元素的序列{k1,k2,…,kn}满足以下关系时,称为堆。
2)代码实现
最小堆:
i>从叶子结点开始,自下而上比较。
ii>先比较left和right谁为最小min,然后将min和结点交换。
iii>上面结点调整后,破坏了下面的小根堆,需重新调整。
最小堆代码如下,实现从大到小的排序。最大堆的实现只需要修改其中两条数据
public class MinHeap {
int[] heap;
int heapsize;
public MinHeap(int[] array) {
this.heap = array;
this.heapsize = heap.length;
buildHeap();
}
public void buildHeap() {
for (int i = heapsize / 2 - 1; i >= 0; i--) {
heapify(i);//依次向上将当前子树最小堆化 对有孩子结点的元素heapify
}
}
public void HeapSort() {//调用这个方法之前,已将数据调整为小根堆
for (int i = 0; i < heap.length; i++) {
//执行n次,将每个当前最大的值放到堆末尾
swap(0, heapsize-1);//取出最小元素,调整剩下的n-1个元素为最小堆
heapsize--;
heapify(0);//重新调整为小根堆
}
}
// 交换元素位置
private void swap(int i, int j) {
int tmp = heap[i];
heap[i] = heap[j];
heap[j] = tmp;
}
public void heapify(int i) {
int l = Left(i);
int r = Right(i);
int smallest=i;
// 存在左结点,且左结点的值小于根结点的值
if (l < heapsize && heap[l] < heap[smallest])//对于大顶堆,只需要改变两个条件之一
smallest = l;
// 存在右结点,且右结点的值小于以上比较的较小值
if (r < heapsize && heap[r] < heap[smallest])//对于大顶堆,只需要改变两个条件之一
smallest = r;
if (smallest == i || smallest >= heapsize)//如果smallest等于i说明i是最小元素 smallest超出heap范围说明不存在比i节点小的子女
return;
// 交换根节点和左右结点中最小的那个值,把根节点的值替换下去
swap(i, smallest);
// 由于替换后左右子树会被影响,所以要对受影响的子树再进行heapify
heapify(smallest);
}
private int Parent(int i) {
return (i - 1) / 2;
}
private int Left(int i) {
return 2 * (i + 1) - 1;
}
private int Right(int i) {
return 2 * (i + 1);
}
// 获取对中的最小的元素,根元素
public int getRoot()
{
return heap[0];
}
// 替换根元素,并重新heapify
public void setRoot(int root)
{
heap[0] = root;
heapify(0);
}
}
topk算法,求最大k个数。在上述算法的基础上,建立topk方法。
// 从data数组中获取最大的k个数
private static int[] topK(int[] data, int k) {
// 先取K个元素放入一个数组topk中
int[] topk = new int[k];
for (int i = 0; i < k; i++) {
topk[i] = data[i];
}
// 转换成最小堆
MinHeap minHeap = new MinHeap(topk);
// 从k开始,遍历data
for (int i = k; i < data.length; i++) {
int root = minHeap.getRoot();
// 当数据大于堆中最小的数(根节点)时,替换堆中的根节点,再转换成堆
if (data[i] > root) {
minHeap.setRoot(data[i]);
}
}
return topk;
}
3)稳定性
不稳定
4)复杂度
①空间复杂度:O(1)。
②时间复杂度:
O(nlogn)
堆排序中建堆过程的时间复杂度是O(n)。
5)举例
①有一组数据(15,9,7,8,20,-1,7,4),用堆排序的筛选方法降序排序建立的初始堆为()
结果为:-1,4,7,8,20,15,7,9
②将整数数组(7-6-3-5-4-1-2)按照堆排序的方式原地进行升序排列,请问在第一轮排序结束之后,数组的顺序是
第一轮结束后的顺序是:6,5,3,2,4,1,7
③ 3,7,2,4,1,5,8 建立大根堆
6)应用
①堆是完全二叉树,但不是:满二叉树、二叉排序树、平衡二叉树。
② 增序排序用“大根堆”,减序排列则要采用“小根堆”。
堆排序的方法:首先,将当前的数组调整为堆,也就是建立堆。然后把根与最后的元素交换,重新调整堆,然后再把调整后的根与倒数第二个元素交换,再重新调整堆,直到全部元素交换完毕。这样,对于大根堆,最大元素排列到了最后,是递增排序。而小根堆,最小元素排列到了最后,是递减排序。
③筛选法建堆
必须从最后一个结点的父结点开始建初始堆。即第n/2个结点。
④对一个堆按层次遍历,不一定能得到一个有序序列。
12, 合并排序(Merging Sort,归并排序)(重要、外部排序)
1)原理
将待排序元素分成大小大致相同的2个子集,分别对2个子集(继续分)进行排序,最终将排好序的子集合并。
原始数据是否有序,对于效率无影响。
可处理大量数据的排序。如:
内存只有100Mb,数据有1Gb,没法一次性放到内存去排序,只能用外部排序,而外排序通常是使用多路归并排序,即将原文件分解成多个能够一次性装入内存的部分(如这里的100Mb),分别把每一部分调入内存完成排序(根据情况选取适合的内排算法),然后对已经排序的子文件进行多路归并排序。
为了减少外存读写次数需要减小归并趟数(外部排序的过程中用到归并),归并趟数为:(其中k为归并路数,n为归并段的个数)。增加k和减小n都可以达到减小归并趟数的目的。置换-选择排序就是一种减小n的、在外部排序中创建初始归并段时用到的算法。它可以让初始归并段的长度增减,从而减小初始归并段的段数(因为总的记录数是一定的)
2)代码实现
递归分治法
public static int[] sort(int[] a, int low, int high) {
int mid = (low + high) / 2;
if (low < high) {
sort(a, low, mid);
sort(a, mid + 1, high);
//左右归并
merge(a, low, mid, high);
}
return a;
}
public static void merge(int[] a, int low, int mid, int high) {
int[] temp = new int[high - low + 1];
int i = low;
int j = mid + 1;
int k = 0;
// 把较小的数先移到新数组中
while (i <= mid && j <= high) {
if (a[i] < a[j]) {
temp[k++] = a[i++];
} else {
temp[k++] = a[j++];
}
}
// 把左边剩余的数移入数组
while (i <= mid) {
temp[k++] = a[i++];
}
// 把右边边剩余的数移入数组
while (j <= high) {
temp[k++] = a[j++];
}
// 把新数组中的数覆盖nums数组
for (int x = 0; x < temp.length; x++) {
a[x + low] = temp[x];
}
}
3)稳定性
稳定。
4)复杂度
①空间复杂度:O(n)。
②时间复杂度:
T(n)=2T(n/2) + O(n) = O(nlogn)
最优情况:两个归并序列,另一个序列都大于第二个序列,只需要比较n次即可合并。
5)举例
13,基数排序(Radix Sorting,桶排序bucket sort)
基数排序(radix sort)属于“分配式排序”(distribution sort),又称“桶子法”(bucket sort)或bin sort。
1)原理
将所有待比较数值(正整数)统一为同样的数位长度,数位较短的数前面补零。然后,从最低位开始,依次进行一次排序。这样从最低位排序一直到最高位排序完成以后, 数列就变成一个有序序列。
可以同时操作多个元素,从而实现了并行处理。
基数排序的方式可以采用LSD(Least significant digital)或MSD(Most significant digital),LSD的排序方式由键值的最右边开始,而MSD则相反,由键值的最左边开始。
①最高位优先(MSD,Most Significant Digit first)法
先按k1排序分组,同一组中记录,关键码k1相等,再对各组按k2排序分成子组,之后,对后面的关键码继续这样的排序分组,直到按最次位关键码kd对各子组排序后。再将各组连接起来,便得到一个有序序列。
②最低位优先(LSD,Least Significant Digit first)法
先从kd开始排序,再对kd-1进行排序,依次重复,直到对k1排序后便得到一个有序序列。
2)代码实现
public static void RadixSort(int[] number, int d){ //d表示最大的数有多少位
int k = 0;
int n = 1;
int m = 1; //控制键值排序依据在哪一位
int[][] temp = new int[10][number.length]; //数组的第一维表示可能的余数0-9
int[] order = new int[10]; //数组orderp[i]用来表示该位是i的数的个数
while (m <= d) {
for (int i = 0; i < number.length; i++) {
int lsd = ((number[i] / n) % 10);
temp[lsd][order[lsd]] = number[i];
order[lsd]++;
}
for (int i = 0; i < 10; i++) {
if (order[i] != 0)
for (int j = 0; j < order[i]; j++) {
number[k] = temp[i][j];
k++;
}
order[i] = 0;
}
n *= 10;
k = 0;
m++;
}
}
3)稳定性
稳定。
4)复杂度
时间效率 :
设待排序列为n个记录,d个关键码,关键码的取值范围为radix,则进行链式基数排序的时间复杂度为O(d(n+radix)),其中,一趟分配时间复杂度为O(n),一趟收集时间复杂度为O(radix),共进行d趟分配和收集。
空间效率:
需要2*radix个指向队列的辅助空间,以及用于静态链表的n个指针。
5)举例
以LSD为例,假设原来有一串数值如下所示:
73, 22, 93, 43, 55, 14, 28, 65, 39, 81
首先根据个位数的数值,在走访数值时将它们分配至编号0到9的桶子中:
0
1 81
2 22
3 73 93 43
4 14
5 55 65
6
7
8 28
9 39
第二步
接下来将这些桶子中的数值重新串接起来,成为以下的数列:
81, 22, 73, 93, 43, 14, 55, 65, 28, 39
接着再进行一次分配,这次是根据十位数来分配:
0
1 14
2 22 28
3 39
4 43
5 55
6 65
7 73
8 81
9 93
第三步
接下来将这些桶子中的数值重新串接起来,成为以下的数列:
14, 22, 28, 39, 43, 55, 65, 73, 81, 93
这时候整个数列已经排序完毕;如果排序的对象有三位数以上,则持续进行以上的动作直至最高位数为止。
LSD的基数排序适用于位数小的数列,如果位数多的话,使用MSD的效率会比较好。MSD的方式与LSD相反,是由高位数为基底开始进行分配,但在分配之后并不马上合并回一个数组中,而是在每个“桶子”中建立“子桶”,将每个桶子中的数值按照下一数位的值分配到“子桶”中。在进行完最低位数的分配后再合并回单一的数组中。
14,Bit排序算法(位排序、bitmap算法)
1)原理
32位机器上,一个整形,比如int a; 在内存中占32bit位,可以用对应的32bit位对应十进制的0-31个数,bitmap算法利用这种思想处理大量数据的排序与查询.
①表示
第一个4就是(从最后一位开始,分别代表0 1 2 3 4 ……)
00000000000000000000000000010000
而输入2的时候
00000000000000000000000000010100
输入3时候
00000000000000000000000000011100
输入1的时候
00000000000000000000000000011110
思想比较简单,关键是十进制和二进制bit位需要一个map图,把十进制的数映射到bit位。
②map映射表
假设需要排序或者查找的总数N=10000000,那么我们需要申请内存空间的大小为int a[1 + N/32],其中:a[0]在内存中占32位可以对应十进制数0-31,依次类推:
bitmap表为:
a[0]--------->0-31
a[1]--------->32-63
a[2]--------->64-95
a[3]--------->96-127
……
那么十进制数如何转换为对应的bit位,下面介绍用位移将十进制数转换为对应的bit位。
③位移转换
对于十进制数n, n/32为对应数组下标;n%32为数组内第几个数。
如:n=24,那么 n/32=0,则24在a[0]数组中,左右往左第n%32=24位的位置上。
利用移位0-31使得对应32bit位为1.
2)优缺点
优点:
1.运算效率高,不许进行比较和移位;
2.占用内存少,比如N=10000000;只需占用内存为N/8=1250000Byte=1.25M。
缺点:
所有的数据不能重复。即不可对重复的数据进行排序和查找。
3)举例
对1亿个不重复的正整数(0-99999999)进行排序,内存只有50MB。(出自《编程珠玑》)
思路:充分利用计算机的每一个bit位来节约空间。如上需求中,我们预设1亿个bit位,初始置0。从待排序集合中每读取一个数,在对应的第多少位bit上置1。最后,遍历该bit位集合,每个为1的bit位,则输出对应的正整数,最终输出结果已经排序完成,所需内存空间仅需:1亿bit/8=12.5MB。
实现方案:使用int数组来模拟bit位序列。1 int = 4 byte = 32 bit,因此需要1亿/32=3125000个int数来表达1亿个bit位。
4)C代码实现
#include <stdio.h>
#define BITSPERWORD 32
#define SHIFT 5
#define MASK 0x1F
#define N 10000000
int a[1 + N/BITSPERWORD];//申请内存的大小
//set 设置所在的bit位为1
//clr 初始化所有的bit位为0
//test 测试所在的bit为是否为1
void set(int i) { a[i>>SHIFT] |= (1<<(i & MASK)); }
void clr(int i) { a[i>>SHIFT] &= ~(1<<(i & MASK)); }
int test(int i){ return a[i>>SHIFT] & (1<<(i & MASK)); }
int main()
{ int i;
for (i = 0; i < N; i++)
clr(i);
while (scanf("%d", &i) != EOF)
set(i);
for (i = 0; i < N; i++)
if (test(i))
printf("%d\n", i);
return 0;
}
windows系统下,在输入回车换行后的空行位置,按 ctrl+z,再回车确认,即可输入EOF。
代码中:
①i>>SHIFT
i右移5位,2^5=32,相当于i/32,即求出十进制i对应在数组a中的下标。
②i & MASK
其中MASK=0X1F,十六进制转化为十进制为31,二进制为0001 1111,i&(0001 1111)相当于保留i的后5位。 相当于i%32
15, TopN算法(重要)
有N(N>>10000)个整数,求出其中的前K个最大的数。(TopK问题,Top10问题)。
1.最简单的方法:将n个数排序,排序后的前k个数就是最大的k个数,这种算法的复杂度是O(nlogn)(选出的这m个数排好序了)
2.**O(n)**的方法(最小复杂度):利用快排的patition思想,基于数组的第m个数来调整,将比第m个数小的都位于数组的左边,比第m个数大的都调整到数组的右边,这样调整后,位于数组右边的m个数最大的数(这m个数不一定是排好序的)(选出的这m个数可能无序) 如下方法(3)
3.**O(nlogk)**的方法:先创建一个大小为m的最小堆,接下来我们每次从输入的n个整数中读入一个数,如果这个数比最小堆的堆顶元素还要大,那么替换这个最小堆的堆顶并调整。如下方法(1)(2)
1)采用小顶堆或者大顶堆
①原理
根据数据前K个建立K个节点的小顶堆,在后面的N-K的数据的扫描中,如果数据大于小顶堆的根节点,则根节点的值覆为该数据,并调节节点至小顶堆。如果数据小于或等于小顶堆的根节点,小根堆无变化。
②场景
求最大K个采用小顶堆,而求最小K个采用大顶堆。
③代码
package sort;
import io.OutputMessage;
public class TopKByHeap {
/**
* 创建k个节点的小根堆
*
* @param a
* @param k
* @return
*/
private static int[] createHeap(int a[], int k) {
int[] result = new int[k];
for (int i = 0; i < k; i++) {
result[i] = a[i];
}
for (int i = 1; i < k; i++) {
int child = i;
int parent = (i - 1) / 2;
int temp = a[i];
while (parent >= 0 &&child!=0&& result[parent] >temp) {
result[child] = result[parent];
child = parent;
parent = (parent - 1) / 2;
}
result[child] = temp;
}
return result;
}
private static void insert(int a[], int value) {
a[0]=value;
int parent=0;
while(parent<a.length){
int lchild=2*parent+1;
int rchild=2*parent+2;
int minIndex=parent;
if(lchild<a.length&&a[parent]>a[lchild]){
minIndex=lchild;
}
if(rchild<a.length&&a[minIndex]>a[rchild]){
minIndex=rchild;
}
if(minIndex==parent){
break;
}else{
int temp=a[parent];
a[parent]=a[minIndex];
a[minIndex]=temp;
parent=minIndex;
}
}
}
public static int[] getTopKByHeap(int input[], int k) {
int heap[] = createHeap(input, k);
for(int i=k;i<input.length;i++){
if(input[i]>heap[0]){
insert(heap, input[i]);
}
}
return heap;
}
}
TopKByHeap.getTopKByHeap(a,3);
2)PriorityQueue优先队列
4)合并法
①原理
实现描述:采用Merge的方法,设定一个数组下标扫描位置记录临时数组和top结果数组,然后从临时数组记录下标开始遍历所有数组并比较大小,将最大值存入结果数组,最大值对应所在数组下标加一存入临时数组,以使其从下位开始遍历,时间复杂度为O(k*m)。(m:为数组的个数)。
###②场景
这种方法适用于几个数组有序的情况,来求Top k。
###③代码
/**
* 已知几个递减有序的m个数组,求这几个数据前k大的数
* a[4,3,2,1],b[6,5,3,1] -> result[6,5,4]
*/
public class TopKByMerge{
public static int[] getTopK(List<List<Integer>>input,int k){
int index[]=new int[input.size()];//保存每个数组下标扫描的位置;
int result[]=new int[k];
for(int i=0;i<k;i++){
int max=Integer.MIN_VALUE;
int maxIndex=0;
for(int j=0;j<input.size();j++){
if(index[j]<input.get(j).size()){
if(max<input.get(j).get(index[j])){
max=input.get(j).get(index[j]);
maxIndex=j;
}
}
}
if(max==Integer.MIN_VALUE){
return result;
}
result[i]=max;
index[maxIndex]+=1;
}
return result;
}
public static void main(String[] args) {
List<Integer> a = new ArrayList<Integer>();
a.add(4);
a.add(3);
a.add(2);
a.add(1);
List<Integer> b = new ArrayList<Integer>();
b.add(6);
b.add(5);
b.add(3);
b.add(1);
List<List<Integer>> ab = new ArrayList<List<Integer>>();
ab.add(a);
ab.add(b);
int r[] = getTopK(ab, 3);
for (int i = 0; i < r.length; i++) {
System.out.println(r[i]);
}
}
}
5)应用
①在一个有8个int数据的数组中,随机给出数组的数据,找出最大和第二大元素一定需要进行()次比较:
解析:
首先,两两比较,找出最大数,需要:n-1 = 7次;
在找出最大数的基础上,找第二大数,比较次数:logn-1 = log8 -1 = 2次。
总共9次。
16,计数排序
1)原理
计数排序对一定量的整数排序时候的速度非常快,一般快于其他排序算法。但计数排序局限性比较大,只限于对整数进行排序。计数排序是消耗空间发杂度来获取快捷的排序方法,其空间发展度为O(K)同理K为要排序的最大值。
用待排序的数作为计数数组的下标,统计每个数字的个数。然后依次输出即可得到有序序列。
例如要排序的数为 7 4 2 1 5 3 1 5;则比7小的有7个数,所有7应该在排序好的数列的第八位,同理3在第四位,对于重复的数字,1在1位和2位(暂且认为第一个1比第二个1小),5和1一样位于6位和7位。
2)代码实现
首先需要三个数组:
一个数组记录A要排序的数列大小为n,
第二个数组B要记录比某个数小的其他数字的个数所以第二个数组的大小应当为K(数列中最大数的大小),
第三个数组C为记录排序好了的数列的数组,大小应当为n。
接着需要确定数组最大值并确定B数组的大小。并对每个数由小到大的记录数列中每个数的出现次数。因为是有小到大通过出现次数可以通过前面的所有数的出现次数来确定比这个数小的数的个数,从而确定其位置。
对于重复的数,每排好一个数则对其位置数进行减减操作,以此对完成其余相同的数字进行排位。
public class CountSort {
public static void countSort(int[] arr) {
if(arr == null || arr.length == 0)
return ;
int max = max(arr);
int[] count = new int[max+1];
Arrays.fill(count, 0);
for(int i=0; i<arr.length; i++) {
count[arr[i]] ++;
}
int k = 0;
for(int i=0; i<=max; i++) {
for(int j=0; j<count[i]; j++) {
arr[k++] = i;
}
}
}
public static int max(int[] arr) {
int max = Integer.MIN_VALUE;
for(int ele : arr) {
if(ele > max)
max = ele;
}
return max;
}
}
3)稳定性
稳定
4)复杂度
其中n为要排序的数的个数,k为要排序的数的最大值。
空间复杂度:O(k)
时间复杂度:O(n+k)。
需要较多辅存空间。
17,大量数据排序(外排序)
数据很大,无法放于内存中排序
①堆排序+分块
假设文件需要分成k块读入,需要从小到大进行排序。
(1)依次读入每个文件块,在内存中对当前文件块进行排序(应用恰当的内排序算法)。此时,每块文件相当于一个由小到大排列的有序队列。
(2)在内存中建立一个最小值堆,读入每块文件的队列头。
(3)弹出堆顶元素,如果元素来自第i块,则从第i块文件中补充一个元素到最小值堆。弹出的元素暂存至临时数组。
(4)当临时数组存满时,将数组写至磁盘,并清空数组内容。
(5)重复过程(3)、(4),直至所有文件块读取完毕。