排序算法实战练习

排序总的来说可以分为两种:

内排序:排序过程全部在内存中进行。
外排序:排序过程需要不断地进行内存和外存之间的数据交换。外部排序时,要将数据分批调入内存来排序,中间结果还要及时放入外存,显然外部排序要复杂得多。

下面主要实战练习内排序。

目录

直接插入排序

(二路)归并排序

希尔排序

 

快速排序

左右指针法

挖坑法

前后指针法

快速排序的优化

堆排序

冒泡排序

直接选择排序 

桶排序



直接插入排序

基本思想:

  把n个待排序的元素看成一个有序表和一个无序表,开始时有序表中只有一个元素,无序表中有n-1个元素;排序过程即每次从无序表中取出第一个元素,将它插入到有序表中,使之成为新的有序表,重复n-1次完成整个排序过程。

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

代码:

#include<iostream>
#include<string>

using namespace std;

template <class ElemType>
class SqList {
private:
	int length = 0;
	ElemType *arr;
public:
	SqList(int length) {
		arr = new ElemType[length];
	}

	SqList() {
		arr = new ElemType[1000];
	}

	void setLength(int n){
		arr = new ElemType[len];
		this->length = len;
	}

	void fillElements(){
		for (int i = 0; i < this->length; i++) {
			cin >> this->arr[i];
		}
	}

	void tranverseList(){
		for (int i = 0; i < length; i++)
			cout << arr[i] << " ";
		cout << endl;
	}

	void initList(){
		int length;
		cin >> length;
		this->setLength(length);
		this->fillElements();
	}

	void SimpleInsertSort();
};

template <class ElemType>
void SqList<ElemType>::SimpleInsertSort() {//直接插入排序核心代码,使用线性表ADT
	for (int i = 1; i < length; i++) {
		int j = 0;
		while (arr[i] > arr[j++] && j < length);
		if (i == --j) {//不需要移动元素,打印当前轮情况,直接比较下一个元素
			tranverseList();
			continue;
		}
		ElemType elemtomove = arr[i];
		int tmp = i;
		while (tmp > j) {
			arr[tmp] = arr[tmp-1];//将比较元素和插入位置之间的元素后移
			tmp--;
		}
		arr[j] = elemtomove;//插入ok的元素
		tranverseList();//打印当前轮情况
	}
}

int main() {
	int dtype;
	cin >> dtype;//第一行输入代表数据类型
	switch (dtype){
		case 0: {
			SqList<int> A;
			A.initList();
			A.SimpleInsertSort();
			break;
		}
		case 1: {
			SqList<double> A;
			A.initList();
			A.SimpleInsertSort();
			break;
		}
		case 2: {
			SqList<char> A;
			A.initList();
			A.SimpleInsertSort();
			break;
		}
		case 3: {
			SqList<string> A;
			A.initList();
			A.SimpleInsertSort();
			break;
		}
		default:
			cout << "err";
			break;
	}
	return 0;
}

 

(二路)归并排序(MergeSort)

归并排序(Merge sort)是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。

时间复杂度:O( nlogn )  二路归并

空间复杂度:O(n)   在每一轮合并相邻两组数据时需要开辟一个临时空间。

该算法的核心思想是二路归并。

二路归并介绍:
<1>在归并的过程中步骤如下:
①设定两个指针(不一定非是指针,只需要记住对应开始下标即可),最初位置分别为两个已经排序序列的起始位置;
②比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置;
③重复步骤3直到某一指针达到序列尾;
④将另一序列剩下的所有元素直接复制到合并序列尾;

动图演示更形象: 

下面采用非递归实现二路归并排序。核心功能为MergeSort()、Merge()函数。其中Merge函数是对相邻两组进行合并,利用元素分块有序的特点,使用空间复杂度为O(n)的方法进行一轮归并。这里的Merge函数里面可以换成其他的排序函数,比如快排。这样省去了每轮的临时空间,但是增加了时间复杂度。 

 

template<class ElemType>
void SqList<ElemType>::Merge(int low, int mid, int high) { //low为第1有序区的第1个元素,mid为第1有序区的最后1个元素,high 为第 2 有序区的最后一个元素。相当于三个指针 //一趟归并排序算法: (两路有序并为一路)  相邻的组进行有序合并
	int l1 = low;
	int l2 = mid+1;
	
	ElemType *temp = new ElemType[high - low + 1];//开辟临时空间,存储归并的相邻自身有序序列
	if (!temp) return;// malloc failed.
	int k=0;//临时数组指示标志

	while (l1 <= mid && l2 <= high) {//把两个相邻自身有序序列填进去,利用本身已经有序的特点
		if (arr[l1] <= arr[l2]) {//因为是递增排序
			temp[k++] = arr[l1++];
		}
		else {
			temp[k++] = arr[l2++];
		}
	}
	//下面复制两组当中某一组还有剩余的,只有一组会有剩余
	while (l1 <= mid) {
		temp[k++] = arr[l1++];
	}
	while (l2 <= high) {
		temp[k++] = arr[l2++];
	}

	for (int i = high; i >= low; i--) {//拷贝回去
		arr[i] = temp[--k];
	}
	delete []temp;//释放临时空间

}

template<class ElemType>
void SqList<ElemType>::MergeSort() {//非递归形式的两路归并排序算法
	int size=1;
	int low=0;
	int mid=0;
	int high=0;
	while (size < length) {//合理描述退出情况
		low = 0;//新一轮开始,从整体的第一个元素开始扫描
		while (low + size < length) {//当前轮中,扫描所有组
			mid = low + size - 1;
			high = mid + size;
			if (high > length - 1) {//检查最后一组是否是正常个数?
				high = length - 1;//最后一组个数不够,直接调整为最后一个
			}
			this->Merge(low, mid, high);//这三个指针表示了相邻的两个组,对这相邻的组进行有序合并。
			low = high + 1;//更新下一对相邻
		}
		size *= 2;//迭代乘2
		tranverseList();
	}
}

 

希尔排序(ShellSort)

原理看图秒懂,本质还是一种插入排序。不过是改进之后的缩小增量插入排序。参考:https://www.cnblogs.com/chengxiao/p/6104371.html

 

看完上面的演示,发现就是多次,分块插入排序。

代码: 

template<class ElemType>
void SqList<ElemType>::ShellInsert(int dk){//dk为增量 
	int i, j;
	ElemType tmp;

	for (i = dk; i < length; i++) {
		tmp = arr[i];
		j = i - dk;
		while (j >= 0 && tmp < arr[j]) {
			arr[j + dk] = arr[j];
			j -= dk;
		}
		arr[j + dk] = tmp;
	}
}

template<class ElemType>//希尔排序核心代码
void SqList<ElemType>::ShellSort(int dlta[], int t){//t为希尔排序的趟数
	for (int i = 0; i < t; i++) {
		ShellInsert(dlta[i]);
		cout << dlta[i] << endl;
		tranverseList();
	}
}

int main() {//测试
	int dtype;
	cin >> dtype;
	int times;
	switch (dtype) {
	case 0: {
		SqList<int> A;
		A.initList();
		cin >> times;
		int *dlta = new int[times];
		for (int i = 0; i < times; cin >> dlta[i], i++);
		A.ShellSort(dlta,times);
		break;
	}
	case 1: {
		SqList<double> A;
		A.initList();
		cin >> times;
		int *dlta = new int[times];
		for (int i = 0; i < times; cin >> dlta[i], i++);
		A.ShellSort(dlta, times);
		break;
	}
	case 2: {
		SqList<char> A;
		A.initList();
		cin >> times;
		int *dlta = new int[times];
		for (int i = 0; i < times; cin >> dlta[i], i++);
		A.ShellSort(dlta, times);
		break;
	}
	case 3: {
		SqList<string> A;
		A.initList();
		cin >> times;
		int *dlta = new int[times];
		for (int i = 0; i < times; cin >> dlta[i], i++);
		A.ShellSort(dlta, times);
		break;
	}
	default:
		cout << "err";
		break;
	}
	return 0;
}

 

快速排序(QuickSort)

使得交换的元素范围变大了。每次选取一个元素作为基准元素,这里默认选取当前段的第一个元素作为基准元素,然后将比这个数小的放到左边,大的放到右边。然后在递归左和右。这实际上,每一轮可以理解为是求解当前轮基准数的位置。

复杂度分析:

https://blog.csdn.net/qq_36528114/article/details/78667034此文讲的挺全面的:

递归实现:

void QuickSort(int* array,int left,int right)
{
    assert(array);
    if(left >= right)//表示已经完成一个组
    {
        return;
    }
    int index = PartSort(array,left,right);//枢轴的位置
    QuickSort(array,left,index - 1);
    QuickSort(array,index + 1,right);
}

PartSort()函数是进行一次快排的算法。 
对于快速排序的一次排序,有很多种算法,我这里列举三种。

左右指针法


选取一个关键字(key)作为枢轴,一般取整组记录的第一个数/最后一个,这里采用选取序列最后一个数为枢轴。
设置两个变量left = 0;right = N - 1;
从left一直向后走,直到找到一个大于key的值,right从后至前,直至找到一个小于key的值,然后交换这两个数。
重复第三步,一直往后找,直到left和right相遇,这时将key放置left的位置即可。


当left >= right时,一趟快速排序就完成了,这时将Key和array[left]的值进行一次交换。 
一次快排的结果:4 1 3 0 2 5 9 8 6 7

基于这种思想,可以写出代码:

int PartSort(int* array,int left,int right)
{
    int& key = array[right];
    while(left < right)
    {
        while(left < right && array[left] <= key)
        {
            ++left;
        }
        while(left < right && array[right] >= key)
        {
            --right;
        }
        swap(array[left],array[right]);
    }
    swap(array[left],key);
    return left;
}



问题:下面的代码为什么还要判断left < right?

while(left < right && array[left] <= key)
1
key是整段序列最后一个,right是key前一个位置,如果array[right]这个位置的值和key相等,满足array[left] <= key,然后++left,这时候left会走到key的下标处。

挖坑法


选取一个关键字(key)作为枢轴,一般取整组记录的第一个数/最后一个,这里采用选取序列最后一个数为枢轴,也是初始的坑位。
设置两个变量left = 0;right = N - 1;
从left一直向后走,直到找到一个大于key的值,然后将该数放入坑中,坑位变成了array[left]。
right一直向前走,直到找到一个小于key的值,然后将该数放入坑中,坑位变成了array[right]。
重复3和4的步骤,直到left和right相遇,然后将key放入最后一个坑位。


当left >= right时,将key放入最后一个坑,就完成了一次排序。 
注意,left走的时候right是不动的,反之亦然。因为left先走,所有最后一个坑肯定在array[right]。

写出代码:

int PartSort(int* array,int left,int right)
{
    int key = array[right];
    while(left < right)
    {
        while(left < right && array[left] <= key)
        {
            ++left;
        }
        array[right] = array[left];
        while(left < right && array[right] >= key)
        {
            --right;
        }
        array[left] = array[right];  
    }
    array[right] = key;
    return right;
}


前后指针法


定义变量cur指向序列的开头,定义变量pre指向cur的前一个位置。
当array[cur] < key时,cur和pre同时往后走,如果array[cur]>key,cur往后走,pre留在大于key的数值前一个位置。
当array[cur]再次 < key时,交换array[cur]和array[pre]。
通俗一点就是,在没找到大于key值前,pre永远紧跟cur,遇到大的两者之间机会拉开差距,中间差的肯定是连续的大于key的值,当再次遇到小于key的值时,交换两个下标对应的值就好了。

带着这种思想,看着图示应该就能理解了。 


下面是实现代码:

int PartSort(int* array,int left,int right)
{
    if(left < right){
        int key = array[right];
        int cur = left;
        int pre = cur - 1;
        while(cur < right)
        {
            while(array[cur] < key && ++pre != cur)//如果找到小于key的值,并且cur和pre之间有距离时则进行交换。注意两个条件的先后位置不能更换,可以参照评论中的解释
            {
                swap(array[cur],array[pre]);
            }
            ++cur;
        }
        swap(array[++pre],array[right]);
        return pre;
    }
    return -1;
}



最后的前后指针法思路有点绕,多思考一下就好了。它最大的特点就是,左右指针法和挖坑法只能针对顺序序列进行排序,如果是对一个链表进行排序, 就无用武之地了。

所以记住了,前后指针这个特点!

快速排序的优化


首先快排的思想是找一个枢轴,然后以枢轴为中介线,一遍都小于它,另一边都大于它,然后对两段区间继续划分,那么枢轴的选取就很关键。

1、三数取中法 
上面的代码思想都是直接拿序列的最后一个值作为枢轴,如果最后这个值刚好是整段序列最大或者最小的值,那么这次划分就是没意义的。 
所以当序列是正序或者逆序时,每次选到的枢轴都是没有起到划分的作用。快排的效率会极速退化。

所以可以每次在选枢轴时,在序列的第一,中间,最后三个值里面选一个中间值出来作为枢轴,保证每次划分接近均等。

2、直接插入 
由于是递归程序,每一次递归都要开辟栈帧,当递归到序列里的值不是很多时,我们可以采用直接插入排序来完成,从而避免这些栈帧的消耗。

整个代码:

//三数取中
int GetMid(int* array,int left,int right)
{
    assert(array);
    int mid = left + ((right - left)>>1);
    if(array[left] <= array[right])
    {
        if(array[mid] <  array[left])
            return left;
        else if(array[mid] > array[right])
            return right;
        else
            return mid;
    }
    else
    {
        if(array[mid] < array[right])
            return right;
        else if(array[mid] > array[left])
            return left;
        else
            return mid;
    }

}

//左右指针法
int PartSort1(int* array,int left,int right)
{
    assert(array);
    int mid = GetMid(array,left,right);
    swap(array[mid],array[right]);

    int& key = array[right];
    while(left < right)
    {
        while(left < right && array[left] <= key)//因为有可能有相同的值,防止越界,所以加上left < right
            ++left;
        while(left < right && array[right] >= key)
            --right;

        swap(array[left],array[right]);
    }

    swap(array[left],key);
    return left;
}

//挖坑法
int PartSort2(int* array,int left,int right)
{
    assert(array);
    int mid = GetMid(array,left,right);
    swap(array[mid],array[right]);

    int key = array[right];
    while(left < right)
    {
        while(left < right && array[left] <= key)
            ++left;
        array[right] = array[left];

        while(left < right && array[right] >= key)
            --right;
        array[left] = array[right];
    }
    array[right] = key;
    return right;
}

//前后指针法
int PartSort3(int* array,int left,int right)
{
    assert(array);
    int mid = GetMid(array,left,right);
    swap(array[mid],array[right]);
    if(left < right){
        int key = array[right];
        int cur = left;
        int pre = left - 1;
        while(cur < right)
        {
             while(array[cur] < key && ++pre != cur)
             {
                 swap(array[cur],array[pre]);
             }
                ++cur;
        }
            swap(array[++pre],array[right]);
            return pre;
    }
    return -1;
}

void QuickSort(int* array,int left,int right)
{
    assert(array);
    if(left >= right)
        return;

    //当序列较短时,采用直接插入
    if((right - left) <= 5)
    InsertSort(array,right-left+1);

    int index = PartSort3(array,left,right);
    QuickSort(array,left,index-1);
    QuickSort(array,index+1,right);
}

int main()
{
    int array[] = {4,1,7,6,9,2,8,0,3,5};
    QuickSort(array,0,sizeof(array)/sizeof(array[0]) -1);//因为传的是区间,所以这里要 - 1;
}



非递归实现
递归的算法主要是在划分子区间,如果要非递归实现快排,只要使用一个栈来保存区间就可以了。 
一般将递归程序改成非递归首先想到的就是使用栈,因为递归本身就是一个压栈的过程。

void QuickSortNotR(int* array,int left,int right)
{
    assert(array);
    stack<int> s;
    s.push(left);
    s.push(right);//后入的right,所以要先拿right
    while(!s.empty)//栈不为空
    {
        int right = s.top();
        s.pop();
        int left = s.top();
        s.pop();

        int index = PartSort(array,left,right);
        if((index - 1) > left)//左子序列
        {
            s.push(left);
            s.push(index - 1);
        }
        if((index) + 1) < right)//右子序列
        {
            s.push(index + 1);
            s.push(right);
        }
    }
}

 

 自己练习:

//快速排序的实现(递归) 
template<class ElemType>
void SqList<ElemType>::QuickSort(int low, int high) {
	int mid;

	if (low >= high) 
		return;
	mid = divide(low, high);
	QuickSort(low, mid - 1);
	QuickSort(mid + 1, high);
}

//快速排序的实现(外壳) 
template<class ElemType>
void SqList<ElemType>::QuickSort() {
	QuickSort(0, getLength() - 1);
}

//划分函数(快速排序) 
template<class ElemType>
int SqList<ElemType>::divide(int low, int high) {
	ElemType k = arr[low];

	do {
		while (low < high && arr[high] >= k)
			high--;
		if (low < high) {
			arr[low] = arr[high];
			++low;
		}
		while (low < high && arr[low] <= k) 
			low++;
		if (low < high) {
			arr[high] = arr[low];
			--high;
		}
	} while (low != high);

	arr[low] = k;
	cout << low << " " << k << endl;
	tranverseList();
	return low;
}

 

堆排序(HeapSort)

堆排序是一种树形选择排序。利用完全二叉树结构当中的双亲节点和孩子节点之间的关系来进行操作(层次遍历编号左孩子2n,右孩子2n+1,完全二叉树的最后一个非终端结点编号必为trunc(n/2))。所以利用线性表存储完全二叉树结构,线性表正向顺序代表完全二叉树的层次遍历结果。

 堆排序需要有两个操作:1、调整当前堆为大/小根堆(升序是大根堆);2、将根节点移动到最后

小根堆(根结点值小于或等于左右孩子的值)

大根堆(根结点值大于或等于左右孩子的值)

建堆

步骤:从最后一个非终端结点(trunc(n/2))开始往前逐步调整,让每个双亲大于(或小于)子女,直到根结点为止。叶子结点没有任何孩子,无需调整。

for(i = length / 2; i > 0; --i)//把a[1..length]构建为大根堆,从下往上,从左到右。

    HeapAdjust(i, length);//局部调整,把a[i..length]构建为大根堆。从结点i开始到堆尾为止,自上向下比较,如果子女的值大于双亲结点的值,则互相交换,即把局部调整为大根堆。

 针对节点i的调整过程:

从结点i开始到当前堆尾m为止,自上向下比较,如果子女的值大于双亲结点的值,则互相交换,将根下降到子女位置继续向下整理

 这样,相当于每轮排序找到了当中数据的最大值,然后拿出来,再继续找剩下当中的最大值。将当前顶点(当前堆最大值)与堆尾记录交换,然后仿建堆动作重新调整,如此反复直至排序结束。

再来个动图更直观: 

HeapAdjust(r, i, m ){
current=i; temp=r[i]; child=2*i; //temp暂存r[i]值,child是其左孩子
   
while(child<=m){         //检查是否到达当前堆尾,未到尾则整理
 if ( child<m && r[child].key<r[child+1].key )
 child= child+1;       //让child指向两子女中的大者位置
 if ( temp.key>=r[child].key ) break; //根大则不必调整,函数结束
 else {   r[current]=r[child];      //否则子女中的大者上移
 current= child; child=2* child; } //将根下降到子女位置并继续向下整理!
           }// while
    r[current]=temp;  //直到自下而上都满足堆定义,再安置入口结点
 }  // HeapAdjust

 自己练习:

template<class ElemType>
void SqList<ElemType>::HeapSort() {//以线性表存储数据,逻辑结构为完全二叉树的层序遍历
	ElemType tmp;

	for (int i = length / 2 - 1; i >= 0; i--)//创建初始的大根堆(升序用大根堆)
		this->HeapAdjust(i, length);//调整每一个非叶节点,从下往上,从左到右

	this->tranverseList();
	
	for (int i = length - 1; i > 0; i--) {//因为每次调整完最大值都在根,所以他的位置已找到,取出来放到最后位置,然后将出去最后节点的堆再调整
		tmp = arr[0];
		arr[0] = arr[i];
		arr[i] = tmp;//swap,已经找到位置的 堆中最大元素, 放到最后,不用管了
		this->HeapAdjust(0, i);//交换后的堆,除去最后一个元素再调整。以此类推

		this->tranverseList();
	}
}

template<class ElemType>
void SqList<ElemType>::HeapAdjust(int hole, int size) {//每一次调整完都会把当前堆中的最大值放在根的位置
	int child;
	ElemType temp = arr[hole];

	while (hole * 2 + 1 < size) {//根据完全二叉树定义
		child = hole * 2 + 1;
		if (child != size - 1 && arr[child + 1] > arr[child])//边界处理child != size - 1 
			child++;
		if (arr[child] > temp)
			arr[hole] = arr[child];
		else
			break;
		hole = child;
	}

	arr[hole] = temp;
}

 

冒泡排序(BubbleSort)

冒泡排序的基本思想是,对相邻的元素进行两两比较,顺序相反则进行交换,这样,每一趟会将最小或最大的元素“浮”到顶端,最终达到完全有序

优点:每趟结束时,不仅能挤出一个最大值到最后面位置,还能同时部分理顺其他元素;一旦下趟没有交换发生,还可以提前结束排序

前提:顺序存储结构

时间效率:O(n^2) :因为要考虑最坏情况

空间效率:O(1) :只在交换时用到一个缓冲单元

稳 定 性: 稳定:两个相同元素(比如25和25*),在排序前后的次序未改变

template <class ElemType>
void SqList<ElemType>::BubbleSort() {
	ElemType t;
	bool isMoved = false;
	for (int i = 1; i < length; i++) {
		isMoved = false;
		for (int j = 0; j < length - i; j++) {//双指针,当前工作区最大的沉到下面了。
			if (arr[j+1] < arr[j]) {//冒泡是两两比较交换的过程
				t = arr[j];
				arr[j] = arr[j+1];
				arr[j+1] = t;//swap,当前元素上浮一个位置
				isMoved = true;
			}
		}
		if (isMoved)
			tranverseList();
	}
}

 


 

直接选择排序 

没啥好讲的了,一直记得这个版本的。看代码:

template<class ElemType>
void SqList<ElemType>::SimpleSelectSort() {
	int _min;
	ElemType t;
	for (int i = 0; i < length - 1; i++) {
		min_index = i;
		for (int j = i + 1; j < length; j++) {
			if (arr[j] < arr[min_index])//往后找后面工作剩余部分当中的最大值。
				min_index = j;
		}
		if (min_index != i) {
			t = arr[i];
			arr[i] = arr[min_index];
			arr[min_index] = t;
		}
		tranverseList();
	}
}

桶排序(BucketSort)

时间复杂度:O(n)
额外空间复杂度:O(n)
稳定

桶排序(Bucket sort)或所谓的箱排序,是一个排序算法,工作的原理是将数组分到有限数量的桶里。每个桶再个别排序(有可能再使用别的排序算法或是以递归方式继续使用桶排序进行排序),最后依次把各个桶中的记录列出来记得到有序序列。当要被排序的数组内的数值是均匀分配的时候,桶排序使用线性时间(O(n))。但桶排序并不是比较排序,他不受到O(n log n)下限的影响。

导引(简易版):统计较小数据范围中重复数据出现的次数。已知要排序数据的范围是0~n(当然范围是m~n+m也都一样)。建立一个一维数组bucket,每一个元素就是一个桶,下标编号bucket[0...n]。在bucket[k]当中存放的值是 k这个数 在要排序数据中出现的次数。这样完成后,扫描bucket当中非0元素即可。

举例,现实问题:有10万个人的年龄要排序,范围0-99。所以开100个桶,下标范围[0..99]。然后扫描这10万个数据。每一次bucket[a[i]]++,完成后再扫描bucket数组即可得到结果。

但是,当数据宽度比较大的时候,再这么做就会发现创建的桶太多了明显不合理。我们肯定还需要去优化它。

基本思想 :桶排序的思想近乎彻底的分治思想。

  1. 桶排序假设待排序的一组数均匀独立的分布在一个范围,并将这一范围划分成几个子范围(桶)
  2. 然后基于某种映射函数f ,将待排序列的关键字 k 映射到第i个桶中 (即桶数组B 的下标i) ,那么该关键字k 就作为 B[i]中的元素 (每个桶B[i]都是一组大小为N/M 的序列 )。补充: 映射函数一般是 f = array[i] / k; k^2 = n; n是所有元素个数
  3. 接着将各个桶中的数据有序的合并起来 : 对每个桶B[i] 中的所有元素进行比较排序 (可以使用快排)。然后依次枚举输出 B[0]….B[M] 中的全部内容即是一个有序序列。

为了使桶排序更加高效,我们需要做到这两点:

  1. 在额外空间充足的情况下,尽量增大桶的数量
  2. 使用的映射函数能够将输入的 N 个数据均匀的分配到 K 个桶中 

所以实现的思路:

  1. 设置一个定量的数组当作空桶子。
  2. 寻访序列,并且把项目一个一个放到对应的桶子去。
  3. 对每个不是空的桶子进行排序。
  4. 从不是空的桶子里把项目再放回原来的序列中。

看图演示,更形象~

这个感觉和哈希表很像了,都需要确定一个映射函数f。 

 假设数据分布在[0,100)之间:

#include<iostream>
#include<string>
 
using namespace std;
 
template <class ElemType>
class SqList {
private:
	int length = 0;
	int max;
	int min;
	ElemType *arr;
	struct LinkNode {
		ElemType data;
		LinkNode *next = NULL;
	};
	LinkNode* *bucket;
	int bucketNumber;
	void insertLinkList(ElemType data, int index);
	void mergeResult();
	int getIndex(ElemType x);

public:
	SqList(int length) {
		arr = new ElemType[length];
	}
 
	SqList() {
		arr = new ElemType[1000];
	}
 
	void setLength(int len){
		this->length = len;
	}
 
	void fillElements(){
		for (int i = 0; i < this->length; i++) {
			cin >> this->arr[i];
		}
	}
 
	void tranverseList(){
		for (int i = 0; i < length; i++)
			cout << arr[i] << " ";
		cout << endl;
	}
 
	void initList(){
		int length;
		cin >> length;
		this->setLength(length);
		this->fillElements();
	}

	ElemType getMax() {
		ElemType max = arr[0];
		for (int i = 1; i < length; i++) {
			if (arr[i] > max) {
				max = arr[i];
			}
		}
		return max;
	}

	ElemType getMin() {
		ElemType min = arr[0];
		for (int i = 1; i < length; i++) {
			if (arr[i] < min) {
				min = arr[i];
			}
		}
		return min;
	}
 
	void BucketSort();
};

template <class ElemType>
int SqList<ElemType>::getIndex(ElemType x) {
	return (x-min) / bucketNumber;
}

template <class ElemType>
void SqList<ElemType>::mergeResult() {
	LinkNode* p;
	int j = 0;
	for (int i = 0; i < bucketNumber; ++i) {
		p = bucket[i];
		while (p != NULL) {
			arr[j++] = p->data;
			p = p->next;
		}
	}
}

template <class ElemType>
void SqList<ElemType>::insertLinkList(ElemType data, int index) {
	if (bucket[index] == NULL) {
		bucket[index] = new LinkNode;
		bucket[index]->data = data;
		bucket[index]->next = NULL;
	}
	else {
		if (data < bucket[index]->data) {//元素需要插入到链表开始的位置的情况
			LinkNode *tmp = new LinkNode;
			tmp->data = data;
			tmp->next = bucket[index];
			bucket[index] = tmp;
			return;
		}
		LinkNode* p;
		p = bucket[index];
		while (p->next != NULL && p->data < data)
			p = p->next;
		LinkNode* t = new LinkNode;//插入链表结点,即放在同一个桶当中
		t->data = data;
		t->next = p->next;
		p->next = t;
	}
}

template <class ElemType>
void SqList<ElemType>::BucketSort() {
	max = getMax(); min = getMin();
	ElemType width = max - min;//确定数据的范围。桶排序需要知道数据的范围

	bucketNumber = width / 10;
	bucket = new LinkNode*[bucketNumber];

	for (int i = 0; i < bucketNumber; ++i)
		bucket[i] = NULL; // init ListArray

	for (int i = 0; i < length; ++i) {
		insertLinkList(arr[i], getIndex(arr[i]));
	}
	mergeResult();

	tranverseList();
}


int main() {
	int dtype;
	cin >> dtype;//第一行输入代表数据类型
	switch (dtype){
		case 0: {
			SqList<int> A;
			A.initList();
			A.BucketSort();
			break;
		}
		case 1: {
			SqList<double> A;
			A.initList();
			A.BucketSort();
			break;
		}
		case 2: {
			SqList<char> A;
			A.initList();
			A.BucketSort();
			break;
		}
		default:
			cout << "err";
			break;
	}
	system("pause");
	return 0;
}

通常情况下,上下界有两种取法,第一种是取一个10^n或者是2^n的数,方便实现。另一种是取数列的最大值和最小值然后均分作桶.。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值