找工作必备基础算法模板之链表+排序

1.链表

操作:建表、反转、合并两个有序的链表,其他简单的删除节点、插入节点就不说了

#include<iostream>
#include<cstdio>
using namespace std;

struct ListNode
{
	int data;
	ListNode *next;
};
//建表
void CreateList(ListNode **pHead)
{
	int n, x;
	printf("请输入链表元素的个数:\n");
	scanf("%d",&n);
	printf("请分别输入各元素\n");
	while (n--)
	{
		scanf("%d",&x);
		ListNode *pNew = new ListNode;
		pNew->data = x;
		pNew->next = NULL;

		//考虑链表为空
		if (*pHead == NULL)
		{
			*pHead = pNew;
		}
		//链表非空
		else 
		{
			ListNode *pNode = *pHead;
			while (pNode->next != NULL)
			{
				pNode = pNode->next;
			}
			pNode->next = pNew;
		}
	}
}
//反转
ListNode *ReverseList(ListNode *pHead)
{
	ListNode *pNewHead = NULL;
	
	while(pHead != NULL)
	{
		//指针调向
		ListNode *pNext = pHead->next;
		pHead->next = pNewHead;

		//指针移位
		pNewHead = pHead;
		pHead = pNext;
	}
	return pNewHead;
}

//两个递增有序链表的合并
ListNode *Merge(ListNode *pHead1, ListNode *pHead2)
{
	//链表为空判断
	if (pHead1 == NULL)
		return pHead2;
	if (pHead2 == NULL)
		return pHead1;
	
	//确定头结点
	ListNode *head = NULL;
	ListNode *p1 = NULL;
	ListNode *p2 = NULL;

	//较小者移一位,较大者不动继续比较
	if (pHead1->data < pHead2->data)
	{
		head = pHead1;
		p1 = pHead1->next;
		p2 = pHead2;
	}
	else 
	{
		head = pHead2;
		p1 = pHead1;
		p2 = pHead2->next;
	}
	//实际维护3个指针,p1,p2,pcurrent

	//pcurrent维护合并后的新链表
	ListNode *pcurrent = head;
	while (p1 != NULL && p2 != NULL)
	{
		if (p1->data < p2->data)
		{
			pcurrent->next = p1;
			pcurrent = p1;
			p1 = p1->next;
		}
		else 
		{
			pcurrent->next = p2;
			pcurrent = p2;
			p2 = p2->next;
		}
	}
	//将链表长度更长者,链接到pcurrent尾端
	if (p1 != NULL)
		pcurrent->next = p1;
	if (p2 != NULL)
		pcurrent->next = p2;
	return head;
}

void PrintList(ListNode *pHead)
{
	ListNode *pNode  = pHead;
	while (pNode != NULL)
	{
		printf("%d ",pNode->data);
		pNode = pNode->next;
	}
	printf("\n");
}
int main()
{
	ListNode *List = NULL;
	CreateList(&List);
	PrintList(List);
	printf("反转链表\n");
	List = ReverseList(List);
	PrintList(List);

	ListNode *List2 = NULL;
	CreateList(&List2);
	printf("合并两个有序链表\n");
	ListNode *ListNew = Merge(List,List2);
	PrintList(ListNew);
	return 0;
}

2.排序(七大排序算法)

可先看下各种排序算法的总结与比较http://blog.csdn.net/arcsinsin/article/details/12581715

交换排序基本思想是:两两比较待排序的元素,发现倒序即交换。包含冒泡排序、快速排序。

冒泡排序:

每趟比较得到一个最小(或最大)的数,每趟比较n - i 次,最多经过n-1趟比较。具体看代码。

与待排序数组的初始序列有关。最少只比较一趟,比较n-1次。

//两种排序的方向:从下往上、从上往下
#include<iostream>
#include<cstdio>
using namespace std;

void BubbleSort1(int a[], int n)
{
	//外层循环控制比较趟数,一般不会有变化.n-1趟
	//内层循环表示每趟比较的情况
	for (int i = 0; i < n-1; i++)
	{
		//从下往上,每趟最小数上浮
		//注意此处j >= i+1,等号不可忽略.比较时a[j]直到a[i+1]
		for (int j = n-2; j >= i; j--) 
		{
			if(a[j] > a[j+1])
				swap(a[j],a[j+1]);
		}
	}
}
void BubbleSort2(int a[], int n)
{
	for (int i = 0; i < n-1; i++)
	{
		//从上往下,每趟最大数下沉
		//此处应没有等号,比较时a[j+1]直到a[n-1-i].有等号结果也不会错,只是每趟多比较一次
		for (int j = 0; j < n-i-1; j++) 
		{
			if (a[j] > a[j+1])
				swap(a[j],a[j+1]);
		}
	}
}

int main()
{
	int a[9]={10,3,2,4,9,5,7,6,8};
	//BubbleSort1(a,9);
	BubbleSort1(a,9);
	for (int i = 0; i < 9; i++)
		cout<<a[i]<<" ";
	cout<<endl;
	return 0;
}

快速排序:

快排的基本思想我就不说了,我分析了下快排的复杂度。

理想情况下,即每次所选的中间元素正好能将子表几乎等分为两部分。

假设对N个元素进行快排,

子表个数

比较次数

1

2

N - 1

2

4

N - 3

3

8

N - 7

4

16

N - 15

logN

N

N-(N-1) = 1

总的比较次数:N*logN - (1+3+7+...+N-1),总的时间复杂度为 O(N*logN)

每趟划分的时间复杂度为O(N),经过logN趟的划分,所以总的时间复杂度为O(N*logN)

#include<iostream>
using namespace std;

void Partition(int a[], int s, int t, int &cutpoint)
{
	int x = a[s];
	int i = s, j = t;
	while (i != j)
	{
		//从后往前搜索小于等于x的数(等号要与不要结果都不会错)
		while (i < j && a[j] > x)j--;
		//在a[i]后面有小于等于x的数,则替换。若没有,则无需做任何操作
		if (i < j)
		{
			a[i++] = a[j];
			//以下将空位置为-1输出,以此可以查看整个快排的过程
			/*a[j] = -1;
			for (int k = 0; k < 7; k++)
			{
				cout<<a[k]<<' ';
			}
			cout<<endl;*/
		}

		//同理,从前往后搜索大于等于x的数
		while (i < j && a[i] < x)i++;
		if (i < j)
		{
			a[j--] = a[i];
			/*a[i] = -1;
			for (int k = 0; k < 7; k++)
			{
				cout<<a[k]<<' ';
			}
			cout<<endl;*/
		}
	}
	a[i] = x;
	cutpoint = i;
}
//without comment
void Partition(int a[], int s, int t, int &cp)
{
	int x = a[s];
	int i = s, j = t;
	while (i != j)
	{
		while (i < j && a[j] > x)j--;
		if (i < j)
		{
			a[i++] = a[j];
		}
		
		while (i < j && a[i] < x)i++;
		if (i < j)
		{
			a[j--] = a[i];
		}
	}
	a[i] = x;
	cp = i;
}
void QuickSort(int a[], int s, int t)
{
	int cp;
	//表中至少还有两个数
	if (s < t)
	{
		//整个快排是在一趟划分之后,对划分出的两个子表分别进行快排
		Partition(a,s,t,cp);
		QuickSort(a,s,cp-1);
		QuickSort(a,cp+1,t);
	}
}
int main()
{
	int a[7] = {3,3,1,4,5,6,2};
	QuickSort(a,0,6);
	for (int i = 0;i < 7; i++)
	{
		cout<<a[i]<<' ';
	}
	cout<<endl;

}

注意,理想情况下,每趟划分正好将表等分成两部分,所以只需要logN趟比较。

但是最坏情况是,每次所选的中间元素是其中最大或最小的元素,一趟划分下来一个子表为空表,另一个子表为原表长度-1,从而需要n-1趟的划分。

所以,极端情况下,快排的复杂度为O(N^2).

快排划分的思想非常重要,有人对其总结为:挖坑填数+分治法。下面是要做的两个面试题。

数组中出现次数超过一半的数 http://ac.jobdu.com/problem.php?pid=1370

最小的K个数 http://ac.jobdu.com/problem.php?pid=1371

归并排序:

归并排序的基本思想是:分治 +  递归

基本操作是将两个有序子表合并为一个有序表---MergeArray。时间复杂度为O(N),N表示两个子表长度之和。

一趟归并排序的时间复杂度为O(N),总共需要logN 趟归并,所以总的时间复杂度为O(N*logN)。最坏的情况下不影响,还是O(N*logN)

下面是我的代码,一个MergeArray操作完成的是first->mid, mid+1->last 两个子表的合并,合并成一个新的有序表。

#include<iostream>
#include<cstdio>
using namespace std;
//对两个有序子表进行合并
void MergeArray(int a[], int first, int mid, int last, int temp[])
{
	int index1 = first;
	int index2 = mid + 1;
	int k = 0;

	while (index1 <= mid && index2 <= last)
	{
		if (a[index1] < a[index2])
		{
			temp[k++] = a[index1++];
		}
		else 
		{
			temp[k++] = a[index2++];
		}
	}

	while (index1 <= mid)
	{
		temp[k++] = a[index1++];
	}
	while (index2 <= last)
	{
		temp[k++] = a[index2++];
	}

	for (int i = 0; i < k; i++)
	{
		a[first+i] = temp[i];
	}
}
//先递归的分解数列,再合并数列,完成整个归并排序
void MergeSort(int a[], int first, int last, int temp[])
{
	int mid = (first + last) / 2;
	if (first < last)
	{
		MergeSort(a,first,mid,temp);//左子表有序
		MergeSort(a,mid+1,last,temp);//右子表有序
		MergeArray(a,first,mid,last,temp);//合并两子表
	}
}
int temp[10];
int main()
{
	int a[10] = {4,5,7,1,9,-1,6,10,3,0};
	MergeSort(a,0,9,temp);
	for (int i = 0; i < 10; i++)
	{
		cout<<a[i]<<' ';
	}
	cout<<endl;
	return 0;
}
注意MergeSort递归是怎么递归的。

详细分析请看之前的一篇文章,

http://blog.csdn.net/arcsinsin/article/details/11861215  重点看递归分治的层次图及我的代码部分。
相关题目练习请自寻。

插入排序

插入排序的基本思想:将待排序表看成是左右两个部分,左边为有序区,右边为无序区。整个排序的过程是将右边无序区中的元素逐个插入到左边的有序区中,以构成新的有序区。插入排序包含两种:直接插入排序、希尔排序

直接插入排序:

思想很简单:就是直接插入。

#include<iostream>
using namespace std;

void InsertionSort(int a[], int n)
{
	int temp, j;
	for (int i = 1; i < n; i++)
	{
		temp = a[i];
		j = i-1;
		//注意j>=0的条件,否则可能越界
		while (j >= 0 && a[j] > temp)
		{
			a[j+1] = a[j];
			j--;
		}
		a[j+1] = temp;
	}
}
void InsertionSort1(int a[], int n)
{
	int j;
	for (int i = 2; i < n; i++)
	{
		//设置a[0]为监视哨
		a[0] = a[i];
		j = i-1;
		//不需要判断下标范围了
		while (a[j] > a[0])
		{
			a[j+1] = a[j];
			j--;
		}
		a[j+1] = a[0];
	}
}
int main()
{
	int a[10] = {0,2,3,1,6,5,7,-1,9,8};
	int b[5]={1,2,3,4,-2147483647};
	InsertionSort1(a,10);
	InsertionSort(b,5);
	int i;
	for (i = 1; i < 10; i++)
	{
		cout<<a[i]<<' ';
	}
	cout<<endl;
	for (i = 0; i < 5; i++)
	{
		cout<<b[i]<<' ';
	}
	cout<<endl;
	return 0;
}
注意设置 监视哨的手法。

什么样的情况是最有利于直接插入排序呢?那就是当序列本来就为正序或者基本有序(表中的逆序的元素较少)。

最坏情况下,数据表为逆序,时间复杂度为O(N^2).

直接插入排序是非常简单的,也很低效,所以有必要进行优化。

优化1:我们发现每次插入操作,是插入到有序表中,于是可以用二分查找搜索插入的位置,以此减少比较的次数。请看http://blog.csdn.net/arcsinsin/article/details/12920191

优化2:先将其分组,每组进行直接插入排序,以使整个序列基本有序,再对整个序列进行直接插入排序。这就是希尔排序。希尔排序是直接插入排序的一种高速而稳定的版本。

希尔排序:

希尔排序也叫缩小增量排序,关键点是如何分组,取步长。一般的做法是取步长n/2,并且对步长取半值直到步长为1。

下面是维基百科对各种取步长的分析:

步长串行[编辑]

步长的选择是希尔排序的重要部分。只要最终步长为1任何步长串行都可以工作。算法最开始以一定的步长进行排序。然后会继续以一定步长进行排序,最终算法以步长为1进行排序。当步长为1时,算法变为插入排序,这就保证了数据一定会被排序。

Donald Shell 最初建议步长选择为\frac{n}{2}并且对步长取半直到步长达到 1。虽然这样取可以比\mathcal{O}(n^2)类的算法(插入排序)更好,但这样仍然有减少平均时间和最差时间的余地。 可能希尔排序最重要的地方在于当用较小步长排序后,以前用的较大步长仍然是有序的。比如,如果一个数列以步长5进行了排序然后再以步长3进行排序,那么该数列不仅是以步长3有序,而且是以步长5有序。如果不是这样,那么算法在迭代过程中会打乱以前的顺序,那就不会以如此短的时间完成排序了。

步长串行 最坏情况下复杂度
{n/2^i}\mathcal{O}(n^2)
2^k - 1\mathcal{O}(n^{3/2})
2^i 3^j\mathcal{O}( n\log^2 n )

已知的最好步长串行是由Sedgewick提出的 (1, 5, 19, 41, 109,...),该串行的项来自 9 * 4^i - 9 * 2^i + 1 和 4^i - 3 * 2^i + 1 这两个算式[1].这项研究也表明“比较在希尔排序中是最主要的操作,而不是交换。”用这样步长串行的希尔排序比插入排序堆排序都要快,甚至在小数组中比快速排序还快,但是在涉及大量数据时希尔排序还是比快速排序慢。

另一个在大数组中表现优异的步长串行是(斐波那契数列除去0和1将剩余的数以黄金分区比的两倍的进行运算得到的数列):(1, 9, 34, 182, 836, 4025, 19001, 90358, 428481, 2034035, 9651787, 45806244, 217378076, 1031612713, …)[2]

代码:

void ShellSort(int a[], int length)
{
	int d = length/2;
	int i, j, temp;
	while (d >= 1)
	{
		for (i = d; i < length; i++)
		{
			temp = a[i];
			j = i - d;
			while (j >= 0 && a[j] > temp)
			{
				a[j+d] = a[j];
				j -= d;
			}
			a[j+d] = temp;
		}
		d /= 2;
	}
}

希尔排序时间性能的分析是一个复杂的问题,和取步长相关。取半步长的方法的时间复杂度为O(N*logN).

更详细讲解可参看:http://hi.baidu.com/gsgaoshuang/item/17a8ed3c24d9b1ba134b14c2

选择排序:

选择排序的基本思想:在每一趟排序中,在待排序子表中选出关键字最小或最大的元素放在其最终的位置。选择排序包含直接选择排序、堆排序。

直接选择排序:

直接选择排序的思路,和直接插入排序不同的是,更多是站在位置的角度来思考,找出最终在该位置上的元素。

void SelectionSort(int a[], int n)
{
	int min;
	for (int i = 0; i < n-1; i++)
	{
		min = i;
		//拿当前的数与后面的所有数比较,找出最小的数
		for (int j = i+1; j < n; j++)
		{
			if (a[j] < a[min])
			{
				min = j;
			}
		}
		//若最小的数就是当前的数那么不交换,否则交换
		if (min != i)
		{
			swap(a[min],a[i]);
		}
	}
}
直接选择排序很直观,时间复杂度为 O(N^2)。造成时间复杂度较大的原因是排序过程中可能存在多次的重复比较。

堆排序:

堆排序是对直接选择排序算法的改进,是利用堆的性质来进行的一种排序。

http://blog.csdn.net/morewindows/article/details/6709644

总结:

各种排序有各自的特点,别忘了回过头看看,各种排序算法的总结与比较http://blog.csdn.net/arcsinsin/article/details/12581715

强烈推荐各种排序算法的动态演示 http://www.cs.usfca.edu/~galles/visualization/ComparisonSort.html


To be continued !



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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值