序算法总结
网上关于排序算法有很多,风格迥异,大家都有自己的习惯与方式。我也结合自己的风格,以及近期的找工作面试情况,试图用简单凝练的方式来记录它们,希望对于读者可以方便快速地掌握。
首先,需要把每种排序的定义记住~–此篇编码内容借鉴自liuyubobo老师
0)桶排序
即假设有11个桶,编号从0~10,每出现一个数,就在对应的编号的桶中放一个小旗子。这种类似桶排序的方法在实际中很好用,但是非常浪费空间,如果需要排序数的范围是0-210000000,那就得需要210000001个桶来存储。
1)冒泡排序(邻居好说话)
简单说就是:每次比较两个相邻的元素,如果它们的顺序错误就把它们交换过来。
定义:由第一个元素开始,比较相邻元素大小,若大小顺序有误,则对调。最坏与平均情况需要比较(n-1)+(n-2)+…+3+2+1 = n(n-1)/2 次。时间复杂度为O(n^2)。
/*冒泡排序*/
/*比较次数(n-1)+(n-2)+...+3+2+1 = n*(n-1)/2*/
void BubbleSort(int array[], int n)
{
for (int i=n-1; i>0; i--) //从n-1、n-2.....1(扫描次数)
{
for (int j = 0; j<i; j++) //每一轮都能找到当前轮里面的最大值,之后就可以不比较这个最大值
{
if (array[j]>array[j+1]) //比较相邻的两个数值
{
swap(array,j,j+1);
}
}
}
}
/*交换函数,作用是交换数组中的两个元素的位置*/
void swap(int array[], int i, int j)
{
int tmp = array[i];
array[i] = array[j];
array[j] = tmp;
}
2)选择排序
定义:由第一个元素依次与后面的元素进行比较,顺序有误则交换。(冒泡是比较相邻的元素)最坏与平均情况需要比较(n-1)+(n-2)+…+3+2+1 = n(n-1)/2 次。时间复杂度为O(n2)。
/*选择排序*/
/*比较次数(n-1)+(n-2)+...+3+2+1 = n*(n-1)/2*/
void SelectionSort(int array[], int n){
for (int i = 0 ; i < n ; i++) {
int minIndex = i; //寻找[i, n)区间里的最小值,然后将最小值放到array[i],接着往后找极小值
for (int j = i + 1 ; j < n ; j++) { //遍历当前轮次里的极小值
if (array[j] < array[minIndex]) {
minIndex = j;
}
}
swap( array, i, minIndex); //将最小值与i交换
}
}
比较冒泡排序、选择排序与插入排序,同样无序的10000个数据所用时间比较
比较冒泡排序、选择排序与插入排序,同样有序的10000个数据所用时间比较
结果为:选择排序<插入排序<冒泡排序(根据所耗的时间)
可见,对于有序数据,插入排序效率提高很快。
结果为:插入排序<选择排序<冒泡排序(根据所耗的时间)
3)插入排序
逐一将数组中的元素与已排序好的元素进行比较,再将该数组元素插入到适当的位置。最坏与平均情况需要比较(n-1)+(n-2)+…+3+2+1 = n(n-1)/2 次。
/*- 6 4 9 8 3 -*/
void InsertSort(int array[], int n)
{
for (int i = 1; i < n; i++) //从位置1开始插入比较,位置0的数为第一次比较的数
{
for (int j = i; j > 0; j--) //没错,就是 j-- ,每次最多需要比较 j 次
{
if (array[j] < array[j-1]) { //如果当前插入点小于前一个点,则交换,确保小的点在前面
swap(array, j, j-1);
}
else {
break; //关键,也是优于选择和冒泡的地方:如果当前插入点大于前一个点,已经是正确的顺序了,此轮不用再排序
}
}
}
}
4)希尔排序
类似于插入排序法,但它可以减少数据移动的次数。排序的原则是将数据划分成特定间隔的几个子集,以插入排序法排完子集内的数据后再逐渐减少间隔的距离。
/*希尔排序*/
/*步长依次伪 n/2,n/2/2,....*/
void ShellSort(int array[], int n)
{
for (int delta = n/2; delta>0; delta /= 2)
{
for (int i = 0; i<delta; i++)
{
for (int j = i + delta; j<n; j += delta)
{
for (int k = j; k>0; k -= delta)
{
if (array[k]<array[k - 1])
swap(array, k, k - 1);
}
}
}
}
}
5)快速排序
在数据中找到一个支点,把小于支点的数据放在左边而大于支点的数据放在右边,再以同样的方式分别处理左右两边的数据。
/*快速排序*/
int __partition(int array[], int l, int r){
int v = array[l]; // 第一个元素设为v,分成后面的数,为 <v 和 >v 的数
int j = l; // j为区分小于和大于v的索引
for ( int i = l + 1 ; i <= r ; i ++ ) { //从l+1开始遍历整个数组
if ( array[i] < v ) { // 当前元素arr[i]小于v,将当前元素arr[i],移动到arr[j+1]位置
j++;
swap(array, j, i);
}
}
swap(array, l, j); // arr[l]为起始位置,arr[j]为中间的key值
return j;
}
// 对arr[l...r]部分进行快速排序,递归调用
void __quickSort(int arr[], int l, int r)
{
if( l >= r ) //关键:递归判断条件
return;
int p = __partition(arr, l, r);//找中间的Key
__quickSort(arr, l, p-1 );
__quickSort(arr, p+1, r);
}
上面讲的 __partition 内容为下图所示,i指向当前元素,l为起始元素,j为Key值的位置
1)如果arr[i] > v, i接着往后走,不需要动
2)如果arr[i] < v, 这是需要将 arr[i] 的值交换到 j+1 的位置
3)最后需要将起始的 arr[l] 元素与 arr[j] 交换,使所有元素在 arr[l] 左边的都比它小,在它右边的都比它大,(因为 arr[l] 为 Key的数值,而 j 为其位置,需要将Key值放到“中间”来,所以需要做这次交换)
6)归并排序 O(logNN),因为二分后形成了O(logN)层级,再用O(N)算法来解决,所以出现了O(logNN)的算法复杂度。
/------------二叉树的遍历------------/
先来简单的递归调用的:
struct TreeNode
{
int val;
TreeNode *left;
TreeNode *right;
TreeNode(int x) : val(x), left(NULL), right(NULL) {}
};
// 前序 BAC(B为根)
void Preorder(TreeNode* ptr)
{
if (ptr != NULL)
{
cout << ptr->val;
Preorder(ptr->left);
Preorder(ptr->right);
}
}
// 中序 ABC(B为根)
void Inorder(TreeNode* ptr)
{
if (ptr != NULL)
{
Inorder(ptr->left);
cout << ptr->val;
Inorder(ptr->right);
}
}
// 后序 ACB(B为根)
void Postorder(TreeNode* ptr)
{
if (ptr != NULL)
{
Postorder(ptr->left);
Postorder(ptr->right);
cout << ptr->val;
}
}
2. 最大公约数与最小公倍数求法
最大公约数用辗转相除法:取两个数中最大的数做除数,较小的数做被除数,用最大的数除较小数,如果余数为0,则较小数为这两个数的最大公约数,如果余数不为0,用较小数除上一步计算出的余数,直到余数为0,则这两个数的最大公约数为上一步的余数。
// 最大公约数
int gcd(int a,int b)
{
int max = a > b ? a:b;
int min = a < b ? a:b;//找最值
int r = 0; //余数
while(max%min!=0) //辗转相除法
{
r = max % min;
max = min;
min = r;
}
return min;
}
为什么可以用辗转相除法?
2、最小公倍数的求法
最小公倍数 = 两整数的乘积÷最大公约数
为啥?
因为,两整数的乘积,即为两数的公倍数,公倍数除以最大公约数,即为最小公倍数,换句话说,该公倍数除以最小公倍数,即为最大公约数。
所以
//最小公倍数
最小公倍数 == a*b/gcd(a,b);
3. 原码、反码、补码
-
原码最高位表示符合位,剩下的位数,是这个数的绝对值得二进制。
int a = 10;
10 的原码表示为 00000000 00000000 0000000 00001010
-10的原码表示为 10000000 00000000 0000000 00001010 -
反码,正数的反码和其原码是一样的,负数的反码就是在其原码的基础上,符号位不变,其他位取反**。
10 的反码表示为 00000000 0000000 00000000 00001010
-10的反码表示为 11111111 1111111 11111111 11110101 -
补码,正数的补码就是原码,负数的补码为其反码的基础上+1
10 的补码表示为 0000000 00000000 0000000 00001010
-10的补码表示为 11111111 1111111 11111111 11111010
–正数的原码、反码、补码都相等。
注:为什么要有原码、反码、补码?解决的是正负相加等于 “0”的问题
例如原码 1 表示为 0001,-1 表示为 1001,两者相加后为1010,计算机认为-2,因而出现了 反码:反码 1表示为 0001,-1 表示为 1110,两者相加后为 1111,1111 在反码中表示为 -0,满足要求,但是会出现两个零的存在 +0(0000) 与 -0(1111),因而引入了补码,从原来的反码的基础上加1,丢掉最高位后,完美地解决了 正负相加等于 “0”的问题,同时不会出现 +0 与 -0的不同表达。
4. 0-1 背包问题
/*
0-1背包问题,问题描述:有编号分别为a,b,c,d,e的五件物品,
它们的重量分别是4,5,6,2,2,它们的价值分别是6,4,5,3,6,每件物品数量只有一个,
现在给你个承重为 10 的背包,如何让背包里装入的物品具有最大的价值总和?
*/
#include <iostream>
#include <algorithm>
#include <vector>
#include <string>
using namespace std;
#define row 100
#define C 10
#define column 100
int f[row][column] = {0};
int W[5] = {4,5,6,2,2};
int V[5] = {6,4,5,3,6};
int main()
{
//初始化1
for(int i=0; i<=5; i++)
{
f[i][0] = 0;
}
//初始化2
for(int j=1; j<=C; j++)
{
f[0][j] = (j > W[0])? V[0]:0;
}
for(i=1; i<=5; i++)
{
for(int y=1; y<=C+1; y++)
{
if(y >= W[i])
{
f[i][y] = ((f[i-1][y] > (f[i-1][y-W[i]] + V[i])) ? f[i-1][y]:f[i-1][y-W[i]] + V[i]);
}
else
{
f[i][y] = f[i-1][y];
}
}
}
for(i=0; i<=5; i++)
{
for(j=0; j<=C+1; j++)
{
cout<< f[i][j]<<" ";
}
cout<<endl;
}
return 0;
}
状态方程 dp( j ) = Max( dp( j ), dp (j-w[i] ) + v[i] ),先放重量为4的物品,依次类推下去,比较价值与收益来决定是否将物体放入背包。