Pat甲级题目刷题分享+算法笔记提炼 ---------------第二部分 排序算法专题(理解各个算法的特性)

前言,因为STL库已经封装好了sort函数,简便的排序一般采用仿函数和函数指针的方法。所以重点要熟悉各个算法的特性。

同样在文章的最后给大家奉献了一个考察各个算法特性的PAT甲级题目,大家看了,就明白了解各个算法的表面作用所在了,

深层次的作用还需要自己在实践中发掘。

#include<vector>
#include<algorithm>
#include<time.h>
using namespace std;
struct Node {
	int x;
	int y;
};

bool  cmp(Node n1, Node n2) {
	if (n1.x<n2.x) { return true; }
	else if (n1.x == n2.x && n1.y<n2.y) { return true; }
	return false;
}
struct S {
	bool operator()(Node n1, Node n2) {
		if (n1.x<n2.x) { return true; }
		else if (n1.x == n2.x && n1.y<n2.y) { return true; }
		return false;
	}
};
int main() {
	vector<Node> nodes;
	srand(time(NULL));
	for (int i = 0; i<10; i++) {
		Node node;
		node.x = rand() % 100;
		node.y = rand() % 100;
		nodes.push_back(node);
	}
	//函数指针方式
	sort(nodes.begin(), nodes.end(), cmp);
	//仿函数方式
	sort(nodes.begin(), nodes.end(), S());
	for (int i = 0; i < nodes.size();i++) {
		printf("%d ",nodes[i].x);
	}
	return 0;
}

1.简单选择排序

 指:对一个序列A中的元素A[0]~A[n-1],令i从0到n-1枚举,进行n趟操作,每趟从待排序部分[i,n-1]中选择最小的元素,与A[i]进行交换。n趟后就有序了


void SelectSort(int num[],int len) {
	for (int i = 0; i < len; i++) {
		int k = i;
		for (int j = i+1; j < len;j++) {
			if (num[j]<num[k]) {
				k = j;
			}
		}
		swap(num[k],num[i]);
	}
}

 从上述可以看出来,每次排完序后,A[0]-A[i]是排好了,但是因为A[i]与A[i]至A[n-1]之间的最小数进行了交换,所以是不稳定的,而且待排序序列各个数的位置也是不断变换,不会始终固定。

2.直接插入排序

指,假设A[0]-A[i-1]已经排好序,轮到A[i]时候就需要在已排序好的序列中找到插入位置,使其插入后A[0]-A[i]是有序的。

void InsertSort(int num[],int len) {
	for (int i = 1; i < len; i++) {
		for (int j = i; j > 0;j--) {
			if (num[j] < num[j - 1]) {
				swap(num[j],num[j-1]);
			}else{
                                break;
                        }
		}
	}
}

从上述代码可以看出,每趟排完后,未排序部分位置始终未发生变化,这就是稳定排序和非稳定排序的区别。

//下面的快排和归并排序均用到two pointer的思想,即两个指针的思想。即对一个待排序序列,使用两个索引同时移动比较操作完成排序过程。

3.快速排序

每次选择一个主元(简单说就是选择一个数),然后对序列进行处理,使得该数左边部分值均不超过他,右边部分均大于他

算法步骤:①将A[1]存储temp临时变量中,并令two pointer left,right 分别指向序列的最左1和右n。

                  ②然后从右边开始,如果A[right]<temp,A[left]=A[right]否则right不断左移。

                  ?再从左边开始,如果A[left]>temp,A[right]=A[left],否则left不断右移。

                  ④不断重复②?直至left=right,最后令A[left]=temp,即可

int Partition(int num[],int left,int right) {
	int temp = num[left];
	while (left<right)
	{
		while (left<right && num[right] > temp) { right--; }
		num[left] = num[right];
		while (left<right && num[left] <= num[right]) { left++; }
		num[right] = num[left];
	}
	num[left] = temp;
	return left; //最后返回主元位置
}

对主元右侧和左侧进行递归调用上述的划分函数,最后就会排好序,时间复杂度为O(nlog(n))

void QuickSort(int num[],int left,int right) {
	if (left < right) {//当前区间长度要大于1
		int pos = Partition(num, left, right);
		QuickSort(num,left,pos-1);
		QuickSort(num,pos+1,right);
	}
}

从上述代码可以看出待排序部分一直在变换,所以也是不稳定的排序算法,但是速度较快

4.归并排序 (2-路归并排序)

基本原理:将序列两两分组,将序列归并为\left \lceil n/2 \right \rceil组,组内单独排序;然后再将这些组再两两归并,生成\left \lceil n/4 \right \rceil组,组内再单独排序,以此类推,直到只剩下最后一组就可以了。时间复杂度为O(nlogn)

对序列{66,12,33,57,64,27,18}

①第一趟:{66,12},{33,57},{64,27},{18},组内单独排序得{12,66},{33,57},{27,64},{18}

②第二趟:两两合并 {12,66,33,57},{27,64,18},组内单独排序后得{12,33,57,66},{18,27,64}

?第三趟:两两合并{12,33,57,66,18,27,64},组内单独排序,得到最终序列

所以可以看出,两组合并时候的合并算法merge很重要。

void merge(int num[],int L1,int R1,int L2,int R2) {
	int i = L1,j=L2;
	int temp[maxn];
	int k = 0;
	while (i<=R1 && j<=R2)
	{
		if (num[i] <= num[j]) {
			temp[k++] = num[i];
			i++;
		}
		else {
			temp[k++] = num[j];
			j++;
		}
	}
	while(i <= R1){temp[k++] = num[i];i++;}
	while (j <= R2) { temp[k++] = num[j]; j++; }
	for (int i = 0; i < k;i++) {
		num[L1 + i] = temp[i];
	}
}

归并排序就是不断对区间进行对半分,直到分到区间内数个数为1为止,之后就是不断和并,用递归很容易实现。

void mergeSort(int num[],int left,int right) {
	if (left < right) {
		int mid = (left + right) / 2;
		mergeSort(num,left,mid);
		mergeSort(num, mid+1, right);
		merge(num,left,mid,mid+1,right);
	}
}

归并排序是把序列递归地分成短序列,递归出口是短序列只有1个元素(认为直接有序)或者2个序列(1次比较和交换),

然后把各个有序的段序列合并成一个有序的长序列,不断合并直到原序列全部排好序。

可以发现,在1个或2个元素时,1个元素不会交换,2个元素如果大小相等也没有人故意交换,这不会破坏稳定性。

那么,在短的有序序列合并的过程中,稳定是是否受到破坏?

没有,合并过程中我们可以保证如果两个当前元素相等时,我们把处在前面的序列的元素保存在结果序列的前面,这样就保证了稳定性。

所以,归并排序也是稳定的排序算法。 

5.堆排序(讲述大顶堆)

大顶堆是一棵完全二叉树,树中每个结点的值都不大于其左右孩子结点的值。

完全二叉树有个特征,读者可以记住:其叶子节点数位\left \lceil \frac{n}{2} \right \rceil,所以非叶子节点数位\left \lfloor \frac{n}{2} \right \rfloor,非叶子节点所在下标为[1,\left \lfloor \frac{n}{2} \right \rfloor]

如果想要知道定理的证明过程可以留言,这里不赘述。

①先用一维数组heap[low,high]存储这个完全二叉树 0下标不能用,这样利用下标index就很容易知道当前节点的父节点index/2和左孩子节点 2*index(<=high),右孩子节点2*index + 1(<=high)

②存储完之后,就从非叶子节点开始下往上从右往左开始,调整这颗树至大顶堆

  具体事例如下:序号3之前的的节点都比其孩子节点值大,所以直接跳转到从3号开始讲述。3号与其左右孩子的最大者也就是99进行比较,比其小,与其交换,但是7号没有孩子节点,交换之后也就处理完了,再处理2号,与其左右孩子的较大者98比较,比其小进行交换,现在4号值为55,然后4号再与其左右孩子的较大者66进行比较,比其小,所以进行交换。再到1号同样处理

上述方法称为:向下调整。downAdjust(int low,int high) 时间复杂度为O(logn)

#include<algorithm>
using namespace std;
const int maxn=1000;
int heap[maxn],n;
void downAdjust(int low, int high) {
	int i = low; //当前节点
	int j = low * 2; //左孩子
	while (j <= high) {
		if (j + 1 <= high && heap[j + 1] > heap[j]) //取左右孩子的最大者
		{ 
			j = j + 1;
		}
		if (heap[i]<heap[j]) {
			swap(heap[i], heap[j]);
			i = j;
			j = i * 2;
		}
		else {
			break;
		}
	}
}

 不断进行向下调整即为构建堆的过程。因此构建大顶堆的代码也很容易得出,这里要注意的是,从非叶子几点开始从下往上从右往左开始构建。所以遍历的时候得这样处理 for(int i = n/2;i>=1;i--)

void createHeap(){
    for(int i=n/2;i>=1;i--){
        downAdjust(i,n);
    }
}

大顶堆构建好之后,根节点显然是序列的最大值,输出根节点,然后将根节点的值与最后一个叶子节点的值进行交换。然后将该

值进行向下调整,调整区间为[1,n-1]就又得到了一个大顶堆,输出根节点,然后重复上述工作直至最后只剩下一个节点为止。

这样heap数组就从小到大排列好了,是不是很神奇。

void heapSort(){
    createHeap(); //建堆
    for(int i=n;i>1;i--){ //枚举,直到堆中只有一个元素
        swap(heap[1],heap[i]); 
        downAdjust(1,i-1);//调整堆顶
    }
}

因为堆排序,已在不断交换未排序(向下调整的时候)部分,所以它也是不稳定的算法。 

以上堆排序也就讲完了,现在再扩展一点,堆相关的知识,也就是堆顶元素的删除 deleteTop以及堆元素的新添insert。

<1>deleteTop

看了堆排序,大家应该很容易猜到删除堆顶如何做,其实很简单,将最后一个叶子节点与堆顶交换,同时修改heap数组的长度n--,然后进行向下调整,调整区间为[1,n-1]

void deleteTop(){
    swap(heap[1],heap[n--]);
    downAdjust(1,n);
}

<2>insert

这个就和上述所有的操作就有些不同,首先将元素放置heap数组的末尾heap[++n],之后就是不断与父节点进行比较大小,如果比父节点大就交换,因为大顶堆的定义知,该父节点比孩子节点的值都大,所以交换之后父节点就不需要与其孩子节点比较大小了。该方法称为向上调整upAdjust(int low,int high)

void upAdjust(int low, int high) {
	int i = high;
	int j = high / 2;
	while (j>=low) {
		if (heap[j] < heap[i]) {
			swap(heap[j],heap[i]);
			i = j;
			j = i / 2;
		}
		else { break; }
	}

}
void insert(int x){
    heap[++n]=x;
    upAdjust(1,n);
}

PAT 排序相关算法题分享:

1098 Insertion or Heap Sort (25 分)

According to Wikipedia:

Insertion sort iterates, consuming one input element each repetition, and growing a sorted output list. Each iteration, insertion sort removes one element from the input data, finds the location it belongs within the sorted list, and inserts it there. It repeats until no input elements remain.

Heap sort divides its input into a sorted and an unsorted region, and it iteratively shrinks the unsorted region by extracting the largest element and moving that to the sorted region. it involves the use of a heap data structure rather than a linear-time search to find the maximum.

Now given the initial sequence of integers, together with a sequence which is a result of several iterations of some sorting method, can you tell which sorting method we are using?

Input Specification:

Each input file contains one test case. For each case, the first line gives a positive integer N (≤100). Then in the next line, N integers are given as the initial sequence. The last line contains the partially sorted sequence of the N numbers. It is assumed that the target sequence is always ascending. All the numbers in a line are separated by a space.

Output Specification:

For each test case, print in the first line either "Insertion Sort" or "Heap Sort" to indicate the method used to obtain the partial result. Then run this method for one more iteration and output in the second line the resuling sequence. It is guaranteed that the answer is unique for each test case. All the numbers in a line must be separated by a space, and there must be no extra space at the end of the line.

Sample Input 1:

10
3 1 2 8 7 5 9 4 6 0
1 2 3 7 8 5 9 4 6 0

Sample Output 1:

Insertion Sort
1 2 3 5 7 8 9 4 6 0

Sample Input 2:

10
3 1 2 8 7 5 9 4 6 0
6 4 5 1 0 3 2 7 8 9

Sample Output 2:

Heap Sort
5 4 3 1 0 2 6 7 8 9

题目大意就是,给定一个未经排序的序列A和一个已经排序好了一部分的序列B,判断这个排序了一部分的序列是用的堆排序算法还是直接插入排序算法。然后再进行下一步排序,输出进行下一步排序后的序列

 现在请读者回顾一下上述的直接插入排序算法和堆排序算法,①堆排序是不稳定的算法,而直接插入排序是稳定排序算法

②堆排序排序好的部分是从后往前的,即每次把最大的放在后面heap[n--]=current_max,而直接插入排序是在已排序好的部分中找到合适的插入位置,所以其前面部分是排序好的,其后面未排序部分与原数组一模一样,因为稳定。

因此可以根据上述两点判断出heapSort和InsertSort

算法思想:直接从InsertSort出手,找到第一个比前面数字小的元素,假设下标为i,将i-n的已经排序了一部分的序列和i-n到为排序序列进行比较,如果完全相同,则是InsertSort,否则就是heapSort

#include<iostream>
using namespace std;
const int maxn = 101;
void downAdjust(int heap[],int low, int high) {
	int i = low; //当前节点
	int j = low * 2; //左孩子
	while (j <= high) {
		if (j + 1 <= high && heap[j + 1] > heap[j]) //取左右孩子的最大者
		{
			j = j + 1;
		}
		if (heap[i]<heap[j]) {
			swap(heap[i], heap[j]);
			i = j;
			j = i * 2;
		}
		else {
			break;
		}
	}
}
int main() {
	int n;
	cin >> n;
	int orgnum[maxn]; //原序列
	int partnum[maxn]; //排过部分的序列
	for (int i = 1; i <= n;i++) {
		cin >> orgnum[i];
	}
	for (int i = 1; i <= n; i++) {
		cin >> partnum[i];
	}
	int pre = partnum[1];
	int index = 1;
	for (int i = 2; i <= n;i++) {
		if (partnum[i] < partnum[i-1]) { index = i; break; }
	}
	bool flag = true; //true为InsertSort false 为heapSort
	for (int i = index; i <= n; i++) {
		if (partnum[i] != orgnum[i]) {
			flag = false;
			break;
		}
	}
	if (!flag) {
		cout << "Heap Sort\n";
		for (int i = n; i > 1;i--) {
			if (partnum[i] < partnum[1]) {
				swap(partnum[i],partnum[1]);
				downAdjust(partnum,1,i-1);
				break;
			}
		}
	}
	else {
		cout << "Insertion Sort\n";
		for (int i = index; i > 1;i--) {
			if (partnum[i] < partnum[i - 1]) {
                             swap(partnum[i], partnum[i - 1]); 
                        }
			else{ break;}
		}
	}
	for (int i = 1; i <= n; i++) {
		if (i == 1) cout << partnum[i];
		else cout << " " << partnum[i];
	}
	return 0;
}

 

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值