数据结构-排序

以下排序均以升序为例。
排序的基本概念
排序:将序列中的元素按照某种要求重新排列。排序一般会有两个操作,比较和移动,通过比较两个关键字的大小确定两个关键字的相对位置,再通过移动调整关键字位置。
算法稳定性:如果序列中有关键字相同的两个元素a,b。排序前a在b前面,排序后a仍在b前面,则这个排序算法是稳定的,否则是不稳定的。
内部排序和外部排序:排序期间元素都在内存中称为内部排序,排序期间需要进行内外存移动的排序称为外部排序。
排序算法可分为五大类:插入排序交换排序选择排序归并排序基数排序

插入排序
思想:插入排序将序列分成两个部分,已排序序部分和未排序部分。每次选择未排序部分第一个元素插入已排序部分。选择的数需要和已排序部分的数进行比较以便确定这个数的位置,再把选择的数插入到这个位置。以增序为例,从未排序序列选择一个数temp,从后往前遍历已排序序列,依次与选择的元素比较,当遇到第一个小于temp的关键字x[i]时,停止遍历。并把x[i]后的数往后移,把选择的数插入到x[i+1]。
时间复杂度O(n^2)空间复杂度O(1)插入排序是稳定的。插入排序适用于顺序表和链表。

折半插入排序:由于插入过程是在一个有序序列上进行的,所以在插入过程中的查找可使用二分查找。时间复杂度O(n^2),折半插入排序是稳定的,但只适用于顺序表。

希尔排序:每次将序列划分为若干个小部分,划分方式为所有下标为i+k*di的元素为一组,i为改组中第一个数的下标,k为整数,d为增量。(意思是间隔为d的元素在同一组,d会从最初选的数减小到1。)组内进行插入排序。因为每一轮都会重新分组,重新分组后的组的数量会减少,组内元素会更多,到最后一轮时只有一组。每次重组后的组是基本有序的,进行希尔排序会减少比较和移动的次数。
时间复杂度取决于增量序列d的改变,最坏为O(n^2),空间复杂度为O(1)。希尔排序会划分子表,可能会改变相同关键字的顺序,希尔排序是不稳定的。希尔排序只适用于顺序表。
例 2 4 3 7 5 8 0
第一轮 选d=3 则分组为(2 7 0) (4 5) (3 8) 排序后 0 4 3 2 5 8 7
第二轮 选d=2 则分组为 (0 3 5 7) (4 2 8) 排序后 0 2 3 4 5 8 7
第三轮 选d=1 则分组为 (0 2 3 4 5 8 7) 排序后为 0 2 3 4 5 7 8
结束。

交换排序
思想:每次比较相邻两个数的大小并根据要求交换两个数的位置。

冒泡排序:如其名字,就像冒泡一样把最大的数交换到最后,就像水中的泡泡浮出水面一样。每次比较相邻两个数的大小,把大的数排到小的数后面,这样一趟比较下来最大的数就到最后去了。进行若干次这样的操作,直到所有的数都有序。
时间复杂度O(n^2),空间复杂度O(1)。冒泡排序是稳定的排序方法。冒泡排序对顺序表和链表都适用。(很少有对链表冒泡排序)

快速排序:每次选择一个基准关键字,把所有小于基准关键字的挪到基准关键字前面,大于的挪到基准关键字后面。再分别对基准前的序列和基准后的序列重复该操作,直到不能划分即有序了。
实现方法 使用递归实现,每次选序列第一个元素为基准temp,设两个指针p1,p2分别指向第一个位置和最后一个位置。p2向左移动,当p2指向位置的元素小于基准元素时,把p2指向位置的元素移动到p1,p1向右移动,当p1指向的数大于等于基准元素时,把p1指向的数移动到p2,p2再往左移。直到p1和p2相遇。让p1指向的位置等于temp,再递归基准元素左右序列。
时间复杂度O(nlogn),最坏情况可达O(n^2)空间复杂度(logn)。最坏情况可达O(n)。快排只适用于顺序表。快排是一种不稳定排序。
例 2 4 3 7 5 8 0
第一轮 选2为基准 排序后 0 2 [3 7 5 8 4]
第二轮 2左边只有一个元素,不必排序,对右边排序 选3为基准 0 2 3 [7 5 8 4]
第四轮 … 选7为基准,0 2 3 [4 5] 7 8
第五轮 …选4为基准,0 2 3 4 5 7 8
结束。

快排代码

#include
#include
#include
using namespace std;


void Qsort(vector<int>& nums,int left,int right)
{
	int temp=nums[left],i=left,j=right;	//temp记录基准数,i为左指针,j为右指针,初始化 
	while(i<j){			//当左指针小于右指针时进行循环 
		while(j>i&&nums[j]>=temp){		//右指针往左寻找小于基准的数 
			j-=1;
		}
		nums[i]=nums[j];			//让左指针元素指向等于右指针的目标元素,可以把左指针指向的地址当作空地址,相当于把右指针找到的数移动到左指针 
		while(i<j&&nums[i]<temp){		//左指针往右寻找大于基准的数 
			i+=1;
		}
		nums[j]=nums[i];		//将左指针找到的数移动到右指针上 
	}
	nums[i]=temp;		//将基准元素放到两指针相遇的地方。 
	if(i-left>1){		//如果基准元素左侧元素个数大于1,则对左侧进行排序(小于等于1就是有序了) 
		Qsort(nums,left,i-1);
	}
	if(right-i>1){		//对右侧元素排序 
		Qsort(nums,i+1,right);
	}
}

int main()
{
	vector<int>nums;
	nums.clear();
	int n,x;
	scanf("%d",&n);
	for(int i=0;i<n;i++){
		scanf("%d",&x);
		nums.push_back(x);
	}
	int len=nums.size();
	Qsort(nums,0,len-1);
	
	for(int i=0;i<len;i++){
		if(i!=0){
			printf(" ");
		}
		printf("%d",nums[i]);
	}
	printf("\n");
	return 0;
}

选择排序
思想:选择排序把序列分为两部分,已排序部分和未排序部分。每轮排序从未排序部分选出最小的数加入到已排序序列末尾,重复直到所有数都已排序。
时间复杂度O(n^2),空间复杂度O(1)。选择排序是一种不稳定排序。选择排序适用于顺序表和链表。

堆排序:堆是一种二叉树形结构,分大小根堆,小根堆的所有非叶节点都小于该节点的子节点,子节点间没什么大小关系。大根堆的所有非叶节点都大于该节点的子节点。堆排序是基于堆的排序。将排序序列构造成一个小根堆,堆的根节点是最小的节点。堆排序相对于普通的选择排序就是加快了寻找未排序序列中最小的元素。将堆顶元素加入已排序序列末尾,重新维护堆重复直到所有数都已排序。
实现方法:用数组存储堆(大根堆),堆是一棵完全二叉树,从下标1开始存堆,此时若节点的下标为i,它的左右子节点(假设存在)的下标为2i和2i+1。构建堆应从下往上调整,从数组的中间开始往前调整(也就是从最后一个非叶节点开始往上调整)。如果子节点大于父节点,则交换,保证父节点大于子节点。建成堆后将根节点与堆末尾交换,再从上往下调整堆,使之再次成为大根堆。
建堆时间复杂度O(n),排序时间复杂度O(nlogn),空间复杂度O(1)。堆排序是一种不稳定的排序。
例 2 4 3 7 5 8 0
建堆 [8 7 3 4 5 2 0]
第一轮 交换8和0,维护堆 [7 5 3 4 0 2] 8
第二轮 交换7和2,维护堆 [5 4 3 2 0] 7 8
第三轮 交换5和0,维护堆 [4 2 3 0] 5 7 8
第四轮 交换4和0,维护堆 [3 2 0] 4 5 7 8
第五轮 交换3和0,维护堆 [2 0] 3 4 5 7 8
第六轮 交换2和0,维护堆 [0] 2 3 4 5 7 8此时未排序的只有一个元,结束排序。
0 2 3 4 5 7 8

堆排序代码(大根堆)

#include
#include
#include
#include
using namespace std;

void DownAdjust(vector<int>&nums,int low,int high)	// 从当前节点下标low-1节点往后调整 
{//因为完全二叉树结构特点是左子节点下标是根节点下标两倍,右子节点下标是左子节点下标加1(用数组存储且从下标为1开始存储) 
	//而nums数组是从0开始,所以需要真实下标需要减一 
	int i=low,j=i*2;
	while(j<=high){			//查看子节点是否存在 
		if(j+1<=high&&nums[j]>nums[j-1]){		// 找出左右子节点中最大值 
			j+=1;
		}
		if(nums[j-1]>nums[i-1]){		//如果子节点最大值比父节点大,交换父节点与子节点。(大根堆) 
			swap(nums[j-1],nums[i-1]);
			i=j;						//因为前面调整了后面也需要调整。 
			j=i*2;
		}else{			//如果子节点小于父节点则跳出。 
			break;
		}
	}
}

void BuildHeap(vector<int>& nums)	//建立堆从最后一个非叶节点开始往上调整(从最后一个非叶节点开始调整,单个节点的调整仍是从上往下)。 
{
	int len=nums.size();
	for(int i=len/2;i>0;i--){ //从最后一个非叶节点开始往上调整
		DownAdjust(nums,i,len);//单个节点的调整依然是从上往下
	}
}

void HeapSort(vector<int>& nums)
{
	int len=nums.size();
	BuildHeap(nums);		//建立堆 
	for(int i=0;i<len;i++){				//循环取出堆中最大元素(直接在原数组上换到数组末尾) 
		swap(nums[0],nums[len-i-1]);		//
		DownAdjust(nums,1,len-i-1);			//交换之后根节点不符合从根节点开始向下调整。 
	}
}

int main()
{
	vector<int>nums;
	nums.clear();
	int n,x;
	scanf("%d",&n);
	for(int i=0;i<n;i++){
		scanf("%d",&x);
		nums.push_back(x);
	}
	int len=nums.size();
	HeapSort(nums);
	
	for(int i=0;i<len;i++){
		if(i!=0){
			printf(" ");
		}
		printf("%d",nums[i]);
	}
	printf("\n");
	return 0;
}

归并排序
(二路归并)思路:将序列分成长度为1的n个序列(一个元素肯定是有序的),将相邻两个有序的序列合并成一个有序序列,重复直到所有序列都合并到一个序列。合并操作因为合并的两个序列都是有序的,每次只需要比较两个序列的最小值选出最小的数即可。
有点类似于希尔排序,与希尔排序不同的是归并排序是合并相邻序列,且每个序列最初都是长度为1的序列,划分序列时不需要对序列内部排序(只有一个数肯定是有序的),只有在合并时才会排序。希尔排序则是根据设置的元素间隔d来确定序列,当序列元素不为1时需要对序列进行排序,并且希尔排序没有初始序列这一说,每一轮都是根据d对序列重新划分。归并排序体现的是将两个(多个)有序序列合并这一思想,希尔排序则是将序列从局部有序一步步到整体有序的过程。
(顺序表存储)时间复杂度O(nlogn),空间复杂度O(n)。归并排序是一种稳定的排序。归并排序只适用于顺序表。(外部排序通常采用归并排序)
实现方法:用顺序表A存储序列,递归每次把序列分成两块,设置三个指针left,mid,right,分别指向第一块开头(left),第一块结尾(mid),第二块结尾(right)。然后再合并这两块区间。区间合并可借助额外一个和原顺序表同等大小的顺序表B先存储要合并的序列区间中的元素,遍历B中的序列,此时两个序列的开头为left和mid+1,比较两个元素找出更小值放入A这两个序列的位置中。
例 2 4 3 7 5 8 0
第一轮 [2 4] [3 7] [5 8] [0]
第二轮 [2 3 4 7] [0 5 8]
第三轮 [0 2 3 4 5 7 8]
结束 0 2 3 4 5 7 8

二路归并排序代码

#include
#include
#include


using namespace std;

vector<int>s;
void Merge(vector<int>& nums,int left,int mid,int right)	//合并两个序列 
{
	int i=left,j=mid+1,k=left;
	for(int i=left;i<=right;i++){			//借助一个辅助数组 
		s[i]=nums[i];
	}
	i=left;
	while(i<=mid&&j<=right){			//比较左右序列大小并填入数组sums 
		if(s[i]<=s[j]){
			nums[k++]=s[i++];
		}else{
			nums[k++]=s[j++];
		}
	}
	while(i<=mid){					//将两个序列中(某个序列肯定会有剩余)剩余的数填入数组,注意这里只能有一个循环会被执行 。 
		nums[k++]=s[i++];
	}
	while(j<=right){
		nums[k++]=s[j++];
	}
}

void MergeSort(vector<int>& nums,int left,int right)		//划分序列 
{
	if(left<right){
		int mid=(left+right)/2;
		MergeSort(nums,left,mid);						//划分左序列 
		MergeSort(nums,mid+1,right);					//划分右序列 
		Merge(nums,left,mid,right);						//合并左右序列 
	}
}

int main()
{
	vector<int>nums;
	nums.clear();
	int n,x;
	scanf("%d",&n);
	for(int i=0;i<n;i++){
		scanf("%d",&x);
		nums.push_back(x);
	}
	int len=nums.size();
	s=nums;
	MergeSort(nums,0,len-1);
	for(int i=0;i<len;i++){
		if(i!=0){
			printf(" ");
		}
		printf("%d",nums[i]);
	}
	return 0;
}

基数排序
思想:对关键字的各个位进行排序。将关键字按该位上的数大小将他们加入不同的序列,然后再按顺序将这些关键字收集。分为最高为优先和最低为优先。
最高位优先

例:456 258 331 452 102 253 203
第一轮,按个位分配
0
1 331
2 452 102
3 253 203
4
5
6 456
7
8 258
9

排序后 331 452 102 253 203 456 258

第二轮,按十位分配
0 102 203
1
2
3 331
4
5 452 253 456 258
6
7
8
9

排序后 102 203 331 452 253 456 258

第三轮,按百位排序
0
1 102
2 203 253 258
3 331
4 452 456
5
6
7
8
9
排序后 102 203 253 258 331 452 456
结束

最低位优先则是先排最高位再排低位。

(链表存储)时间复杂度O(d(n+r)),空间复杂度O(r)d为排序的次数(关键字的最大位数),r为关键字的进制数(比如以上例子为10进制数,r=10),n为数列元素个数。基数排序是稳定的排序算法。*

你可能感兴趣的:(面试算法,c语言,算法)