数据结构与算法-排序算法总

       所谓排序就是将待排序文件中的记录,按关键字非递增或者非递减次序排列起来。即将一组“无序”的记录序列调整成为“有序”的记录序列。记录是进行排序的基本单位,它由若干个数据项组成。其中有一项可用来唯一标识一条记录,称为关键字项,该数据项的值称为关键字key,关键字的选取根据实际问题的需求而定。

        排序的稳定性:通俗的说就是排序结束后,相同关键字的相对位置和排序前是一样的没有发生更改,那么就是稳定的排序算法,反之为不稳定的。

        排序的分类,根据排序时数据所占用存储器的不同可分两大类:内排序和外排序。平日中用的快速排序,堆排序,冒泡等等这都是内排序,整个排序过程全在内存里面完成。而整个排序过程需要借助外存辅助才能完成,数据量非常大的时候,内存一次性放不下这么多数据,就需要内、外存交换,先从外存里读一些数据到内存里排好序完了放进外存里,这样一部分一部分完成整个排序的过程,最后是一个合并的过程,称为外部排序。

        内排序是首要掌握的,内排序主要分四大类:插入排序、选择排序、交换排序、归并排序。至于分配类的排序,像计数排序、桶排序可以不做细讲。

 

       

   排序算法的性能评价:执行时间和所需要的辅助空间,算法本身的复杂程度。若一个排序算法所需要的辅助空间并不依赖于问题的规模,也就是辅助空间为O(1),则称为就地排序,非就地排序一般要求的辅助空间为O(n)。大多数排序算法的时间消耗主要是,关键字之间的比较,记录的移动,尤其是数组之间的移动极为耗时。

1、插入排序

  • 直接插入排序

        整体的思想是逐个把未排序的序列插入到已经排好序的序列中合适的位置,主要过程包括,从已排序的序列中找合适的插入位置,找到了的话,就插入在该位置,数组的插入就涉及移动元素,没找到就说明当前要插入的数据大于等于已排序的序列所有元素,不用插入,保持在原地不动。

        整个过程和打牌整理手上牌相似,摸上来第一张牌不用整理,此后每次摸上来的牌(无序区)插入到手中的牌(有序区)中正确的位置上,每次插入前寻找这个正确的位置,应将摸来的牌和手中的牌自左向右的逐一比大小,直到摸完牌为止,就可以得到一个有序的牌。

插入排序
像是玩朴克一样,我们将牌分作两堆,每次从后面一堆的牌抽出最前端的牌,然后插入前面一
堆牌的适当位置,例如:
排序前:92 77 67 8 6 84 55 85 43 67
[77 92] 67 8 6 84 55 85 43 67 将77插入92前
[67 77 92] 8 6 84 55 85 43 67 将67插入*77前
[8 67 77 92] 6 84 55 85 43 67 将8插入67前
[6 8 67 77 92] 84 55 85 43 67 将6插入8前
[6 8 67 77 84 92] 55 85 43 67 将84插入92前
[6 8 55 67 77 84 92] 85 43 67 将55插入67前
[6 8 55 67 77 84 85 92] 43 67 ......
[6 8 43 55 67 77 84 85 92] 67 ......
[6 8 43 55 67 67 77 84 85 92] 
 */
//插入排序是稳定的排算法
void DirectInsertSorted(int *array, int array_length) {
	int i;
	int j;
	int t;
	int temp;

	for(i = 1; i < array_length; i++) {
		temp = array[i];
		for(j = 0; j < i && array[j] <= array[i]; j++);
		for(t = i; t > j; t--) {
			array[t] = array[t - 1];
		}
		array[t]  = temp;
	}
}

 

  •     希尔排序

 

        希尔排序是(ShellSort)又称为“缩小增量排序”,是1959年有D.L.Shell提出来的,基本思路是:将整个待排序元素序列划分成若干个子序列,分别进行直接插入排序,然后依次缩减增量再进行排序,待整个序列中的元素基本有序时,再对全体元素进行一次直接插入排序。直接插入排序在元素基本有序的情况下,效率是很高的,因此希尔排序在事件效率上比直接插入排序有很大的提高。

        而这个增量的设置,一开始设置为序列长度的一半,然后每次减一,直到这个增量等于0的时候结束。

        每一次产生了交换需要判断一下是否影响到了前面的序列。

void shellSort(int *array, int array_length) {
	int i,j;
	int gap;

	for(gap = array_length / 2; gap > 0; gap /= 2) {
		for(i = gap; i < array_length; i++) {
			for(j = i - gap; j >= 0 && array[j] > array[j + gap]; j -= gap) {
				swapData_way1(&array[j], &array[j + gap]);
				showArray(array, array_length);
			}
		}
	}
}

 

2.选择排序

 

 

  • 简单选择排序

 

        选择类排序的基本思想是:每次在未排序的序列里面找到一个最小的元素,挨个从数组前面开始放。主要就是如何从剩余的元素里面找出最小或者最大的元素项。

 

        简单选择排序是最容易想的一个,每次就从有序序列的后面一个最小的元素,放在排序序列里面,通过交换这一步完成,直到把所有的元素放完位置。

选择排序
将要排序的对象分作两部份,一个是已排序的,一个是未排序的,从后端未排序部份选择一个
最小值,并放入前端已排序部份的最后一个,例如:
排序前:70 80 31 37 10 1 48 60 33 80
[1] 80 31 37 10 70 48 60 33 80 选出最小值1
[1 10] 31 37 80 70 48 60 33 80 选出最小值10
[1 10 31] 37 80 70 48 60 33 80 选出最小值31
[1 10 31 33] 80 70 48 60 37 80 ......
[1 10 31 33 37] 70 48 60 80 80 ......
[1 10 31 33 37 48] 70 60 80 80 ......
[1 10 31 33 37 48 60] 70 80 80 ......
[1 10 31 33 37 48 60 70] 80 80 ......
[1 10 31 33 37 48 60 70 80] 80 ...... 
 选择排序不是稳定的排序 */

void selectedSort(int *array, int array_length) {
	int i; 
	int j;
	int min;

	for(i = 0; i <  array_length; i++) {
		min = i;
		for(j = i; j < array_length; j++) {
			min = array[j] < array[min] ? j : min;
		}
		if(min != i) {
			swapData(&array[i], &array[min]);			
		}
		
	}
}

 

  • 堆排序

 

        堆排序将数组的序列看成一个完全二叉树,根节点为在数组中的下标为ri,那么其左右孩子对应下标分别为2*ri + 1, 2 * ri + 2,哪个下标超出了数组下标范围就说明这个节点没有该孩子。

         完成堆排序总共有两大步,要得到一个升序的序列的话。

            1.初始化大堆: 根据数组序列构造的完全二叉树调整成一个大根堆。

            2.剪枝和维护堆:将二叉树的根节点移到数组后面,也就是把根节点的数据和堆尾交换,并且堆中的数量减一(剪枝),然后维护当前堆使其仍保持是一个大堆。

void adjustHeap(int *array, int count, int root) {
	int leftChild;
	int rightChild;
	int whichChild;


	while(root <= count/2 - 1) {

		leftChild = 2 * root + 1;
		rightChild = ((leftChild + 1) >= count) ? -1 : (leftChild + 1);

		whichChild = (rightChild == -1) ? leftChild : (array[leftChild] > array[rightChild] ? leftChild : rightChild);
		whichChild = array[root] > array[whichChild] ? -1 : whichChild;

		if(-1 == whichChild) {
			return;
		}
		swapData(&array[root], &array[whichChild]);

		root = whichChild;
	}
}

//通过堆排序完成升序 先初始化大顶堆
//然后交换堆顶的和对重最后一个元素 继续维护大顶堆 不断去交换
//最后完成的堆是一个小顶堆
void heapSort(int *array, int array_length) {
	int root;

	for(root = array_length / 2 - 1; root >= 0; root--) {
		adjustHeap(array, array_length, root);
	}
	swapData(&array[0], &array[array_length - 1]);
	for(array_length--; array_length > 1; array_length--) {
		adjustHeap(array, array_length, 0);
		swapData(&array[0], &array[array_length - 1]);
	}
}

 

3.交换类排序

 

 

  • 冒泡排序

        冒泡排序可能是第一个接触的排序算法,冒泡冒泡,轻的往上走,重的自行往下沉。用冒泡排序来完成一个数组的排序,两两比较,大的项向后走,小的项向前走,一趟的冒泡排序可以把一个元素放到最终的位置不再发生改变。

        

void BubbleSort(int *array, int array_length) {
	int i;
	int j;

	for(i = 0; i < array_length - 1; i++) {
		for(j = 0; j < array_length - i - 1; j++) {
			if(array[j] > array[j + 1]) {
				swapData(&array[j], &array[j + 1]);
			}
		}
	}
}

 

  • 改良后的冒泡

 

           从上面普通的冒泡排序可以看出在第四趟冒泡结束之后整个数组就已经有序了,后面的几趟并没有做什么有用的功,后面几趟的遍历并没有元素,也就是白白了循环了这么多次。

            而改良后的冒泡排序加入了检测一趟冒泡下来是否交换元素,如果有某一趟没有产生交换元素的行为,那么就不必再继续了,这个序列已经排好了,再接着循环也是白白浪费时间。对于某些元素序列可以提前结束外层循环,但最坏情况如果给的虚了是一个完全降序的序列的情况,循环的情况是和普通的冒泡相同的。

void ShakerSort(int *array, int array_length) {
	int i;
	int j;

	for(i = 0; i < array_length - 1; i++) {
		boolean hasSwapflag = 0;
		for(j = 0; j < array_length - i - 1; j++) {
			if(array[j] > array[j + 1]) {
				swap(&array[j], &array[j + 1]);
				hasSwapflag = 1;
			}
		}
		if(!hasSwapflag) {
			break;
		}
	}
}

 

  • 快速排序

 

        从冒泡排序可见,每次扫描只能对相邻的两个记录进行比较,是从序列的一头到另一头单向的进行,因为做一次交换只能消除一个逆序,如果能通过一次交换可以消除多个逆序,那么必将加快排序的速度。

        快速排序的每一趟给出一个中间轴,然后一个指针从头向后走遇到大于中间轴的关键字停下来,一个指针从尾向前走遇到小于中间轴关键字的停下来,然后交换头尾指针位置的关键字,直到头尾指针相遇或者头指针超过了尾指针就结束这一趟快速排序,然后递归对此时头指针左边的序列和尾指针右边的序列继续进行同样的快速排序。

每一趟快排后的结果以及头尾指针停止时在数组中的下标

解法这边所介绍的快速演算如下:将最左边的数设定为轴,并记录其值为 s
廻圈处理:
令索引 i 从数列左方往右方找,直到找到大于 s 的数
令索引 j 从数列左右方往左方找,直到找到小于 s 的数
如果 i >= j,则离开回圈d
如果 i < j,则交换索引i与j两处的值
将左侧的轴与 j 进行交换
对轴左边进行递回
对轴右边进行递回
说明在快速排序法(一)中,每次将最左边的元素设为轴,而之前曾经说过,快速排序法的
加速在于轴的选择,在这个例子中,只将轴设定为中间的元素,依这个元素作基准进行比较,
这可以增加快速排序法的效率。
解法在这个例子中,取中间的元素s作比较,同样的先得右找比s大的索引 i,然后找比s小的
索引 j,只要两边的索引还没有交会,就交换 i 与 j 的元素值,这次不用再进行轴的交换了,
因为在寻找交换的过程中,轴位置的元素也会参与交换的动作,例如:
41 24 76 11 45 64 21 69 19 36
首先left为0,right为9,(left+right)/2 = 4(取整数的商),所以轴为索引4的位置,比较的元素是
45,您往右找比45大的,往左找比45小的进行交换:
41 24 76* 11 [45] 64 21 69 19 *36
41 24 36 11 45* 64 21 69 19* 76
41 24 36 11 19 64* 21* 69 45 76
[41 24 36 11 19 21] [64 69 45 76]
完成以上之后,再初别对左边括号与右边括号的部份进行递回,如此就可以完成排序的目的。
 */
void quickSortOnce(int *array, int left, int right);

void quickSortOnce(int *array, int left, int right) {
	int i;
	int j;
	int s;
	static int count = 0;

	if(left < right) {
		s = array[(left + right) / 2];
		i = left - 1;
		j = right + 1;
		while(1) {
			while(array[++i] < s);
			while(array[--j] > s);
			if(i >= j){
				break;
			}
			swapData(&array[i], &array[j]);		
		}
		printf("for %dth time quickSort: mid = %d, low = %d, high = %d  ", ++count, s, i , j);
		showArray(array, 9);
		quickSortOnce(array, left, i - 1);// 对停止循环后i的左右进行递归
		quickSortOnce(array, j + 1, right);	//对j的右侧进行递归  
	}
}

void quickSort(int *array, int array_length) {
	quickSortOnce(array, 0, array_length - 1);
}

 

 

4.归并排序

 

 

 

  • 二路归并排序

        归并排序时首先将原始无序序列划分划分,直到划分成每一个序列只包含一个元素,可以视为包含一个元素序列的序列有序,然后再合并序列,整个过程是划分序列和合并序列。

        而合并两个有序的序列的过程,比较序列头,取出较小的进入结果序列,接着继续比较,直到其中一个序列为空,结束比较的过程,如果还有一个序列没放进来完,把该序列的所有元素再放进来。和两个有序链表的合并思想完全相同。在合并两个有序的数组的时候需要额外申请一块空间来完成这个合并的过程。

//二路归并排序
void Divided(int *array1, int first, int last, int *array2) {
	if(first < last) {
		int mid = (first + last) / 2;
		Divided(array1, first, mid, array2);
		Divided(array1, mid + 1, last, array2);
		mergeArray(array1, first, mid, last, array2);
	}
}

void mergeArray(int *array1, int first, int mid, int last, int *array2) {
	int i = first;
	int j = mid + 1;
	int m = mid;
	int n = last;
	int k = 0;

	while(i <= m && j <= n) {
		array2[k++] = array1[i] <= array1[j] ? array1[i++] : array1[j++];
	}

	while(i <= m) {
		array2[k++] = array1[i++];
	}
	while(j <= n) {
		array2[k++] = array1[j++];
	}

	for(i = 0; i < k; i++) {  //将array2的值返回给array1
		array1[first + i] = array2[i];
	}
}

void MergeSort(int *array, int array_length) {
	int *array2;

	array2 = (int *)calloc(sizeof(int), array_length);
	Divided(array, 0, array_length - 1, array2);

	free(array2);
}

 

5.计数排序和桶排序

 

        这一类的排序主要的思想是对元素分类、空间换时间的思想。

        计数排序把数组的值当做下标,去统计序列中各个关键字的出现次数,遍历这个统计各个关键字出现次数的序列放进原数组,就自然而然的完成了一个排序。计数排序如果要排序的数值是分布在很小范围的整数,那么计数排序会很好用。一旦涉及大范围的数据或者高精度数字浮点数,这种排序就很难再直接去使用,浮点数得放大成整数才能正确的使用下标。

        在完成计数排序的时候需要申请一个足够大的数组,这个数组的长度取决于要排序序列的极差。

//计数排序主要思想是将待排序序列的值当做下标,在另外一个数组中进行统计
//有类似统计一段字符串中各个字符出现的频率,也是空间换时间的思想
//新申请的数组长度是待排序序列中max-min+1,min当做一个偏移量
//计数排序对于待排序序列数值分布在小范围有很好的效果,前提是整数,这很关键,浮点数无法去寻找下标,除非放大浮点数

//主要是找出数组array中的最大值最小值,传递countSort_Tool函数去申请空间
void countSort(int *array, int array_length);

void countSort_Tool(int *array, int array_length, int maxValue, int minValue);

void countSort_Tool(int *array, int array_length, int maxValue, int minValue) {
	int *countArray;
	int countArray_length = maxValue - minValue + 1;
	int i;
	int index = 0;

	countArray = (int *)calloc(sizeof(int), countArray_length);
	//进行计数 注意有一个偏移量minvalue
	for(i = 0; i < array_length; i++) {
		countArray[array[i] - minValue]++;
	}

	for(i = 0; i < countArray_length; i++) {
		while(countArray[i] > 0) {
			array[index++] = i + minValue;
			countArray[i]--;
		}
	}

	kwen_free(countArray);
}

void countSort(int *array, int array_length) {
	int max;
	int min;
	int i;

	for(i = 1, max = array[0], min = array[0]; i < array_length; i++) {
		max = max > array[i] ? max : array[i];
		min = min < array[i] ? min : array[i];
	}

	countSort_Tool(array, array_length, max, min);

}

 

        桶排序将序列中的数字按照大小范围划分到各个桶里面去,单个桶里的元素有序依次放入,最后把各个桶里的元素再合并起来还给数组。具体实现可以通过数组加链表,数组来表示多个桶,链表表示单个桶里面的元素。

 

// 桶由链表和数组构成  count负责记录这个桶由几个元素 也就是链表的长度
typedef struct BUCKET{
	int count;
	struct LinkList *list;
}BUCKET;

//记住bucket只是一个结构体数组
void destoryBucket(BUCKET *bucket, int BucketCount);
void initBucket(BUCKET *bucket, int BucketCount);
void insertDataToBucket(int *array, int array_length, BUCKET *bucket, int minValue);
void BucketDataToArray(int *array, int array_length, BUCKET *bucket, int BucketCount);
void BUCKETSORT(int *array, int array_length);

void BucketDataToArray(int *array, int array_length, BUCKET *bucket, int BucketCount) {
	int index = 0;
	int i;
	LinkList *p;

	for(i = 0; i < BucketCount && index < array_length; i++) {
		p = bucket[i].list->next;
		while(p != NULL) {
			array[index++] = p->data;
			p = p->next;
		}
	}
}

void insertDataToBucket(int *array, int array_length, BUCKET *bucket, int minValue) {
	int i;
	int index;
	LinkList *p;

	for(i = 0; i < array_length; i++) {
		LinkList *node;
		index = (array[i] - minValue) / array_length;
		p = bucket[index].list;    //每次让p指向当前桶的头结点
		node = (LinkList *)calloc(sizeof(LinkList), 1);
		node->data = array[i];
//将数组的数据升序的放进链表 考虑链表是否为空
//为空的话直接放在头结点后面
//不为空的话需要找要插入位置的前驱节点
		if(bucket[index].count == 0) {
			node->next = p->next;
			p->next = node;
		}else {	
			p = (array[i] > (p->next->data)) ? p->next : p;		 
			//如果此时一个桶是H->6,让5进桶,此时p还是指向head,
			//如果5大于头结点的下一个节点值,再让p移动 不然的话p不懂还是指向头
			//进入下面循环后发现5不大于6 p也没有移动还是在头结点 插入5到头结点后面
			while(p->next && (array[i] > p->next->data)) {
				p = p->next;
			}	
			node->next = p->next;
			p->next = node;	
		}
		bucket[index].count++;
	}
}

void initBucket(BUCKET *bucket, int BucketCount) {
	int i;

	for(i = 0; i < BucketCount; i++) {
		bucket[i].count = 0;
		initLinkList(&(bucket[i].list));
	}
}

void destoryBucket(BUCKET *bucket, int BucketCount) {
	int i;

//销毁每一个桶里面的链表
	for(i = 0; i < BucketCount; i++) {
		destoryLinkList(&(bucket[i].list));
	}

	free(bucket);
	bucket = NULL;
}

void BUCKETSORT(int *array, int array_length) {
	int BucketCount;
	int maxValue;
	int minValue;
	BUCKET *bucket;

	maxValue = getArrayMaxValue(array, array_length);
	minValue = getArrayMinValue(array, array_length);

	BucketCount = (maxValue - minValue + array_length) / array_length;
	bucket = (BUCKET *)calloc(sizeof(BUCKET), BucketCount);

	initBucket(bucket, BucketCount);
	insertDataToBucket(array, array_length, bucket, minValue);
	BucketDataToArray(array, array_length, bucket, BucketCount);

	destoryBucket(bucket, BucketCount);
}

 

测试各个排序的时间

 

将排序的函数名装在一个结构体,通过指向函数的指针去访问

const SortFunction ALLSORT[] = {
	quickSort,
	MergeSort,
	heapSort,
	shellSort,
	countSort,
	// BUCKETSORT,
	selectedSort,
	DirectInsertSorted,
	BubbleSort_high,
	BubbleSort_low
};

const char* sortName[] = {
	"QuickSort",
	"MergeSort",
	"HeapSort",
	"ShellSort",
	"countSort",
	// "BUCKETSORT",
	"SelectedSort",
	"InsertSort",
	"BubbleSort_high",
	"BubbleSort_low"
};

const int ALLSORT_LENGTH = sizeof(ALLSORT) / sizeof(SortFunction);

 

准备数据函数

 

int *createArray(int count, int maxValue, int minValue)  {
	int *result = NULL;
	int index = 0;

	result = (int *)calloc(sizeof(int), count);

	srand(time(NULL));
	for(index = 0; index < count; index++) {
		result[index] = rand()%(maxValue - minValue + 1) + minValue;
	}

	return result;
}

测试函数

#include<stdio.h>

#include"KWENSORTTOOLS.h"


int main(void) {
	long before_time;
	long after_time;
	long total_Time;
	int i;
	const int randomDataCeil = 1000;
	const int randomDataLow = 1;
	const int array_length = 100000;
	int *array;

	for(i = 0; i < ALLSORT_LENGTH; i++) {
		array = createArray(array_length, randomDataCeil, randomDataLow);
		before_time = clock();
		allsort(array, array_length, i);
		after_time = clock();
		total_Time = after_time - before_time;
		printf("%d datas %s :  %ld.%03ld s\n",
		 array_length, sortName[i], total_Time / 1000, total_Time % 1000);
	}
	// showArray(array, array_length);

	kwen_free(array);

	return 0;
}

 

测试分布在1-1000之间100000个数据各个排序时间消耗  简单选择冒泡实在太慢 先测试10万个数据

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值