数据结构与算法笔记
ADT(抽象数据类型)
链表
为了便于第一个元素的插入和删除,链表通常加一个表头,表头不存数据,表头位置在0处.
在删除元素时需要记住被删元素的前一个表元.双向链表方便地获取前一个元素,便于删除.
栈
栈(stack)是限制插人和删除只能在一个位置上进行的表,该位置是表的末端,叫做栈的顶(top)。对栈的基本操作有Push (进栈)和Pop(出栈),Top(获取值).
栈可以用链表(通常是单链表),数组等实现.
对符号的成对检测:
public boolean isValid(String s) {
char chs[]=s.toCharArray();
if(chs.length==0)
return true;
if(chs==null||chs[0]==')'||chs[0]==']'||chs[0]=='}')
return false;
if(chs.length%2==1)
return false;
Stack<Character> stack=new Stack<Character>();
for(char t:chs) {
try{
switch (t) {
case '(':
case '{':
case '[':
stack.push(t);
break;
case ')':
if(stack.peek()=='(') {
stack.pop();
} else {
return false;
}
break;
case '}':
if(stack.peek()=='{') {
stack.pop();
} else {
return false;
}
break;
case ']':
if(stack.peek()=='[') {
stack.pop();
} else {
return false;
}
break;
}
} catch(EmptyStackException e){
return false;
}
}
if(stack.size()!=0) return false;
return true;
}
队列
从队头入队,队尾出队,基本操作有入队、出队、查看队头元素。
队列可以用数组、链表实现。
散列
根据键值映射到对应值的地址,从而获取值.查找效率比链表高,插入与删除效率比数组高
散列函数(Hash函数)负责键到地址的映射,是把任意长度的输入通过散列算法变换成固定长度的输出,该输出就是散列值。这种转换是一种压缩映射,也就是,散列值的空间通常远小于输入的空间,不同的输入可能会散列成相同的输出,所以不可能从散列值来确定唯一的输入值。
理想的散列表是固定大小的数组.
如果不同键映射到了相同地址则发生了冲突,冲突是避免不了的,但应尽量减少发生,并提供处理冲突的方法.如果键的数大于散列表长度则必会发生冲突.
常见的冲突处理方法:
- 开发地址法.如果发生冲突则尝试其下一个地址,如果不冲突则完成,如果冲突重复以上步骤.需要额外变量保存偏移量.
- 拉链法(分离链接法).
如果冲突则加上链表. - 再哈希法
常用散列函数:
1.直接寻址法。取关键字或关键字的某个线性函数值为散列地址。即H(key)=key或H(key) = a·key + b,其中a和b为常数(这种散列函数叫做自身函数)
2. 数字分析法。分析一组数据,比如一组员工的出生年月日,这时我们发现出生年月日的前几位数字大体相同,这样的话,出现冲突的几率就会很大,但是我们发现年月日的后几位表示月份和具体日期的数字差别很大,如果用后面的数字来构成散列地址,则冲突的几率会明显降低。因此数字分析法就是找出数字的规律,尽可能利用这些数据来构造冲突几率较低的散列地址。
3. 平方取中法。取关键字平方后的中间几位作为散列地址。
4. 折叠法。将关键字分割成位数相同的几部分,最后一部分位数可以不同,然后取这几部分的叠加和(去除进位)作为散列地址。
5. 随机数法。选择一随机函数,取关键字作为随机函数的种子生成随机值作为散列地址,通常用于关键字长度不同的场合。
6. 除留余数法。取关键字被某个不大于散列表表长m的数p除后所得的余数为散列地址。即 H(key) = key MOD p,p<=m。不仅可以对关键字直接取模,也可在折叠、平方取中等运算之后取模。对p的选择很重要,一般取素数或m,若p选的不好,容易产生碰撞。
树
N个节点的树有N-1条边.
从节点n1到nk的路径的长为路径上边的条数,即k-1.
对任意节点ni,其深度为从根节点到ni的路径的长,其高为ni到一片树叶的最长路径.
树可以用链表实现,将子节点放在链表中.
前序遍历:先输出该节点,后输出做左孩子,再输出又孩子.
中序遍历:先输出左孩子,后输出该节点,再输出右孩子.
后序遍历:先输出左孩子,后输出右孩子,再输出该节点.
二叉树
每个节点不能多于两个儿子.
平均二叉树的深度比N小得多.
可以在节点上加上两个指向其他节点的指针实现.
完全二叉树:除底层外全部被填满.
满二叉树:底层也被填满.
二叉查找树
对于任意节点X,其左子树的所有关键字都小于X,其右子树的所有关键字都大于X.
public class BinarySearchTree {
private BinaryTreeNode root;
public void insert(int x,BinarySearchTree tree) {
if(tree.root==null) {
tree.root=new BinaryTreeNode(x);
}
else if(x<tree.root.getData()) {
BinarySearchTree leftSubTree=new BinarySearchTree(tree.root.getLeftChild());
insert(x,leftSubTree);
tree.root.setLeftChild(leftSubTree.root);
}
else if(x>tree.root.getData()) {
BinarySearchTree rightSubTree=new BinarySearchTree(tree.root.getRightChild());
insert(x,rightSubTree);
tree.root.setRightChild(rightSubTree.root);
}
}
public BinaryTreeNode find(int x,BinaryTreeNode node) {
if(node==null) return null;
if(x<node.getData()) return find(x, node.getLeftChild());
else if(x>node.getData()) return find(x, node.getRightChild());
else return node;
}
public BinaryTreeNode findMin1(BinaryTreeNode node) {//递归实现
if(node==null) return null;
else if(node.getLeftChild()!=null) return findMin1(node.getLeftChild());
else return node;
}
public BinaryTreeNode findMin2() {//循环实现
if(root==null) return null;
BinaryTreeNode tnode=root;
while(tnode.getLeftChild()!=null) {
tnode=tnode.getLeftChild();
}
return tnode;
}
public void preOrder(BinaryTreeNode node) {//前序遍历
if(node!=null) {
System.out.println(node.getData());
preOrder(node.getLeftChild());
preOrder(node.getRightChild());
}
}
public void midOrder(BinaryTreeNode node) {//中序遍历
if(node!=null) {
midOrder(node.getLeftChild());
System.out.println(node.getData());
midOrder(node.getRightChild());
}
}
public void postOrder(BinaryTreeNode node) {//后序遍历
if(node!=null) {
postOrder(node.getLeftChild());
postOrder(node.getRightChild());
System.out.println(node.getData());
}
}
public BinarySearchTree(BinaryTreeNode root) {
this.root=root;
}
public BinaryTreeNode getRoot() {
return root;
}
public void setRoot(BinaryTreeNode root) {
this.root = root;
}
public static void main(String[] args) {
BinarySearchTree tree=new BinarySearchTree(null);
tree.insert(5, tree);
tree.insert(3, tree);
tree.insert(2, tree);
tree.insert(4, tree);
tree.insert(8, tree);
tree.insert(6, tree);
tree.insert(9, tree);
System.out.println("pre:");
tree.preOrder(tree.root);
System.out.println("mid:");
tree.midOrder(tree.root);
System.out.println("post:");
tree.postOrder(tree.root);
}
}
class BinaryTreeNode {
private int data;
private BinaryTreeNode leftChild;
private BinaryTreeNode rightChild;
public BinaryTreeNode(int data) {
this.data=data;
}
public int getData() {
return data;
}
public void setData(int data) {
this.data = data;
}
public BinaryTreeNode getLeftChild() {
return leftChild;
}
public void setLeftChild(BinaryTreeNode leftChild) {
this.leftChild = leftChild;
}
public BinaryTreeNode getRightChild() {
return rightChild;
}
public void setRightChild(BinaryTreeNode rightChild) {
this.rightChild = rightChild;
}
}
输出结果:
查找二叉树中序遍历输出为升序排列.
查找二叉树的插入、删除、查找操作理想为O(logN)
平衡二叉查找树(AVL树)
AVL树是其每个节点的左子树和右子树的高度最多差1的二叉查找树(空树的高度定义为-1,只有根的树的高度为0).
要在每一个节点保存高度信息.
对于高度为h的AVL树,最少节点数S(h)=S(h-1)+S(h-2)+1,对于h=0,S(0)=1;h=1,S(1)=2.与斐波那契数密切相关.
除去可能的插入外,所有的树操作时间复杂度为O(logN).
插入新节点可能平衡AVL树的平衡,因此需要一些操作来恢复平衡,这些操作就叫旋转.
把必须重新平衡的节点叫做a。由于任意节点最多有两个儿子,因此高度不平衡时,a点的两棵子树的高度差2。容易看出,这种不平衡可能出现在下面四种情况中:
1.对a的左儿子的左子树进行一次插入。
2.对a的左儿子的右子树进行一次插入。
3.对a的右儿子的左子树进行一次插人。
4.对a的右儿子的右子树进行一次插入。
情形1和4是关于a点的镜像对称,而2和3是关于a点的镜像对称。因此,理论上只有两种情况,当然从编程的角度来看还是四种情形。
第一种情况是插人发生在“外边”的情况(即左-左的情况或右-右的情况),该情况通过对树的一次单旋转( single rotation)而完成调整。第二种情况是插人发生在“内部"的情形(即左-右的情况或右-左的情况),该情况通过稍微复杂些的双旋转(doublerotation)来处理。我们将会看到,这些都是对树的基本操作,它们多次用于平衡树的一些算法中。
红黑树
历史上AVL树流行的另一变种是红黑树(red black tree)。对红黑树的操作在最坏情形
下花费O(logN)时间,而且我们将看到,(对于插人操作的)一种慎重的非递归实现可以相对
容易地完成(与AVL树相比)。
红黑树是具有下列着色性质的二叉查找树:
1.每一个节点或者着成红色,或者着成黑色。
2.根是黑色的。
3.如果一个节点是红色的,那么它的子节点必须是黑色的。
4.从一个节点到一个NULL指针的每一条路径必须包含相同数目的黑色节点。
着色法则的一个推论是,红黑树的高度最多是2log(N+ 1)。因此,查找保证是一种对数
的操作。
红黑树的变换:
改变颜色
左旋:父节点下移一层变成左孩子,右孩子上移一层变成父节点,其左孩子变成原父节点的右孩子.
右旋:类似左旋.
所有插入点默认为红色.
1.变颜色的情况:当前结点的父亲是红色,且它的祖父结点的另个子结点也是红色。( 叔叔结点) :
(1)把父节点设为黑色
( 2 )把叔叔也设为黑色
( 3 )把祖父也就是父亲的父亲设为红色(爷爷)
(4 )把指针定义到祖父结点设为当前要操作的.(爷爷)分析的点变换的规则
2.左旋:当前父结点是红色,叔叔是黑色的时候,且当前的结点是右子树。左旋
以父结点作为左旋。
3.右旋:当前父结点是红色,叔叔是黑色的时候,且当前的结点是左子树。右旋
( 1 )把父结点变为黑色
(2)把祖父结点变为红色(爷爷)
(3)以祖父结点旋转(爷爷)
B树(B-树)
BTree,B-Tree :B树N叉的排序树
M阶的Btree的几个重要特性:
1.结点最多含有m颗子树(指针),m-1个关键字(存的数据,空间( m>=2) ;
2.除根结点和叶子结点外,其它每个结点至少有ceil(m/ 2)个子节点,ceil为上取整;
3.若根节点不是叶子节点,则至少有两颗子树
树的根或者是一片树叶,或者其儿子数在2和M之间。
除根外,所有非树叶节点的儿子数在[M/2]和M之间。
所有的树叶都在相同的深度上。
可以理解为B树每个节点有多个关键字,每个关键字被两个子树夹着,关键字左边子树的所有关键字小于它,右边的大于它.
B+树
B+树是B树的变体.
一颗m阶B+树具有如下特征:
1.根结点至少有两个子女。
2.每个中间节点都至少包含ceil(m/ 2) 个孩子,最多有m个孩子。
3.每一个叶子节点都包含k-1个元素,其中m/2 <=k<= m。
4.所有的叶子结点都位于同一层。
5.每个节点中的元素从小到大排列,节点当中k-1个元素正好是k个孩子包含的元素的值域分划。
B+树中数据都存在叶子节点中,内部节点存的数据只是为了划分区域,便于查找.
例如:
B节点数据都小于等于8,C节点的数据都在8~15之间;同样地,D的数据小于等于2,E的数据在2 ~5之间,F的数据在5 ~8之间.父节点中的元素都出现在子节点中,是子节点中的最大或最小元素.
每个叶子节点有一个指针指向下一个叶子节点,形成有序链表.
B+树优势:
使用叶子节点存储数据,查询需达到叶子节点,使查询效率稳定;叶子节点形成有序链表,便于范围操作;B+树将数据放在叶子节点,将使树的层数更低,到叶子节点的I/O次数就更少,减少读取磁盘次数.
正是由于以上优势,B+树被用于实现文件系统目录,Mysql索引等.
优先队列
两种基础操作:插入和删除最小者.
PriorityQueue<Integer> queue=new PriorityQueue<Integer>();
二叉堆(堆)
堆也是优先队列.
是一颗完全二叉树.通常用数组表示.
对于数组中任意位置i上的元素,其左儿子在2i上,右儿子在左儿子的后一个单元(2i+1),父亲在2/i上.
堆可以用一个数组、数组长度、堆大小表示。
对于每一个节点x,x的父节点的关键字小于x的关键字.
递归
基本法则:
- 基准情形.必须总有某些基准的情形,它们不要递归就能求解.
- 不断推进.递归调用必须总能朝着产生基准情形的方向推进.
- 设计法则.假设所有的递归调用都能运行.
- 合成效益法则.切勿在不同的递归调用中做重复性的工作.
插入排序
时间复杂度为O(n^2)
//插入排序,假定前p个已排好序,只需将后面的插入的前面适当位置
int main(int argc, char const *argv[])
{
int a[5]={4,1,3,6,7};
int i;
insertSort(a,5);
for ( i = 0; i < 5; i++)
{
printf("%d\n", a[i]);
}
getchar();
return 0;
}
void insertSort(int a[],int n){
int i,j;
int tmp;
for(i=1;i<n;i++){
tmp=a[i];
for(j=i;j>0 && a[j-1]>tmp;j--){
a[j]=a[j-1];//其他元素后移
}
a[j]=tmp;//把元素插过去
}
}
冒泡排序
从头开始遍历,与下一个进行比较,若更大则交换,这样最后一个就是最大的,在重复操作找到第二大的……时间复杂度为O(N²)
#include <stdio.h>
#include <stdlib.h>
//冒泡排序
int main(int argc, char const *argv[])
{
int a[10]={1,4,7,4,7,1,6,3,5,4};
int i,t,n;
for(n=9;n>0;n--){
for (i = 0; i < n; i++)
{
if(a[i]>a[i+1]){
t=a[i];
a[i]=a[i+1];
a[i+1]=t;
}
}
}
for (i = 0; i < 10; i++)
{
printf("%d\t", a[i]);
}
system("pause");
return 0;
}
希尔排序
最坏情况时间为O(n^2).
int main(int argc, char const *argv[])
{
int a[5]={4,6,2,8,5};
int i;
shellSort(a,5);
for(i=0;i<5;i++){
printf("%d\n", a[i]);
}
getchar();
return 0;
}
void shellSort(int a[],int n){
int i,j,increment;
int tmp;
for(increment=n/2;increment>0;increment/=2){
for(i=increment;i<n;i++){
tmp=a[i];
for(j=i;j>=increment;j-=increment)
if(tmp<a[j-increment])
a[j]=a[j-increment];
else
break;
a[j]=tmp;
}
}
}
归并排序
采用分治思想,将一个大问题划分为容易解决的小问题.
将数组分为两半,分别递归地调用自身,使两个数组有序,再将这两个数组归并为一个数组.
递归的结束条件是分割到只有一个,因为一位数可以认为是有序的.
归并两个有序数组的做法是分别从头开始遍历并比较,将较小的放入新数组,直到一个数组遍历完成,将另一个剩余的元素直接加入新数组,此时新数组有序.
时间复杂度为:O(n log n)
#include <stdio.h>
#include <stdlib.h>
void merge(int arr[],int tmp[],int left,int mid,int right);
void sort(int arr[],int tmp[],int left,int right);
int main(){
int arr[10]={4,8,3,4,9,3,7,2,5,6},tmp[10];
int i;
sort(arr,tmp,0,9);
free(tmp);
for(i=0;i<10;i++){
printf("%d\t", arr[i]);
}
getchar();
return 0;
}
void sort(int arr[],int tmp[],int left,int right){
int mid;
if(left<right){//当数组划分到只有一个时结束递归.因为一位数可以认为是有序的.
mid=(left+right)/2;
sort(arr,tmp,left,mid);
sort(arr,tmp,mid+1,right);
merge(arr,tmp,left,mid,right);
}
}
void merge(int arr[],int tmp[],int left,int mid,int right){
int count=right-left+1;
int rpos=mid+1,tpos=left,i;
while(left<=mid && rpos<=right){
if (arr[left]<arr[rpos])
{
tmp[tpos++]=arr[left++];
} else {
tmp[tpos++]=arr[rpos++];
}
}
while(left<=mid){
tmp[tpos++]=arr[left++];
}
while(rpos<=right){
tmp[tpos++]=arr[rpos++];
}
for (i = 0; i < count; i++,right--)
{
arr[right]=tmp[right];
}
}
快速排序
选取枢纽元,从枢纽元左边寻找比它大的,进行交换;再从右边寻找比它小的,进行交换……当遍历完所有元素后,该枢纽元的位置就确定到了,其左边所有元素都比它小,右边元素都比它大,再分别对其左右递归地进行快速排序。
上面有频繁的交换,实现的时候不必每次都真的交换,可以先用一个变量把枢纽元保存起来,等找到其位置时再放过去.
选取枢纽元应是最左边、最右边、中间三数的中位数,以下为了简便直接用第一个数为枢纽元。
时间复杂度为O (nlogn)
void QuickSort(int array[], int low, int high) {
int i = low;
int j = high;
int key = array[i];
if (low < high) {
while (i < j) {
while (i < j && array[j] >= key) {//这里必须要有=,否则如果数组有相同元素将陷入无限循环
j--;
}
if (i < j) {
array[i] = array[j];
}
while (i < j && array[i] <= key) {
i++;
}
if (i < j) {
array[j] = array[i];
}
}
array[i] = key;
int standard = i;
QuickSort(array, low, standard - 1);
QuickSort(array, standard + 1, high);
}
}
int main(int argc, char const *argv[])
{
int a[N]={5,4,7,4,7,1,6,3,5,4};
int i;
QuickSort(a,0,N-1);
for (i = 0; i < N; i++)
{
printf("%d\t", a[i]);
}
getchar();
return 0;
}
字符串转float
#include <stdio.h>
#include <math.h>
//字符串转float
int main(int argc, char const *argv[])
{
char *str="123.34";
int numstr[10]={0};
int xpos,t;
float result=0;
int i,j;
for ( i = 0; str[i]!='\0'; i++)
{
if (str[i]!='.')
{
numstr[i] = str[i]-48;
} else {
xpos=i;
}
}
t=xpos;//保存小数点位置
for (j = 0; j<i; j++)
{
if (j!=xpos)
{
result+=numstr[j]*pow(10,t-1);
t--;
}
}
printf("%f\n", result);
getchar();
return 0;
}
幂运算
#define IsEven(x) x%2
//求x^n,如果n为偶数,x^n=x^n/2*x^n/2,如果为奇数x^n=x^(n-1/2)*x^(n-1/2)*x
long int Pow(long int x,int n){
if(n==0)
return 1;
if(n==1)
return x;
if(IsEven(n))
return Pow(x,(n-1)/2)*Pow(x,(n-1)/2)*x;
else
return Pow(x,n/2)*Pow(x,n/2);
}
//如果n为偶数,x^n=x^2*n/2,如果为奇数x^n=x^2*(n-1)/2*x,如果n为奇数,(n-1)/2=n/2
long int Pow2(long int x,int n){
if(n==0)
return 1;
if(n==1)
return x;
if(IsEven(n))
return Pow(x*x,n/2)*x;
else
return Pow(x*x,n/2);
}
求最大子串和
数组包含正负.求数组从i到j(i<=j)的最大和.
最优算法,时间复杂度为O(N):
动态规划
int maxSubSum(const int a[],int n){
int thisSum,maxSum,j;
thisSum=maxSum=0;
for (j = 0; j < n; j++)
{
thisSum+=a[j];
if (thisSum>maxSum)
maxSum=thisSum;
else if(thisSum<0)//如果thisSum<0则舍弃
thisSum=0;
}
return maxSum;
}
分治思想,递归法,将数组分成两个,最大子序列要么在左边,要么在右边,要么跨越中间.递归求解。第三种情况的最大和可以通过求出前半部分的最大和(包含前半部分的最后一个
元素)以及后半部分的最大和(包含后半部分的第一个元素)而得到。然后将这两个和加在一起,时间复杂度为O(NlogN):
#define Max3(x,y,z) x>y?(x>z?x:z):(y>z?y:z)
int maxSubSum2(const int a[],int left,int right){
int maxLeftSum,maxRightSum;
int maxLeftBorderSum,maxRihtBorderSum;
int leftBorderSum,rightBorderSum;
int center,i;
if (left==right)//数组只有一个元素
{
if (a[left]>0)
return a[left];
else
return 0;
}
center=(left+right)/2;
maxLeftSum=maxSubSum2(a,left,center);
maxRightSum=maxSubSum2(a,center+1,right);
maxLeftBorderSum=0;leftBorderSum=0;
for (i = center; i >= left; i--)
{
leftBorderSum+=a[i];
if (leftBorderSum>maxLeftBorderSum)
maxLeftBorderSum=leftBorderSum;
}
maxRihtBorderSum=0;rightBorderSum=0;
for (i = center+1; i <= right; i++)
{
rightBorderSum+=a[i];
if(rightBorderSum>maxRihtBorderSum)
maxRihtBorderSum=rightBorderSum;
}
return Max3(maxLeftSum,maxRightSum,maxLeftBorderSum+maxRihtBorderSum);
}
两次循环,计算并比较所有可组和的情况,时间复杂度为O(N²):
int maxSubSum3(const int a[],int n){
int thisSum,maxSum,i,j;
maxSum=0;
for (i = 0; i < n; i++)
{
thisSum=0;
for (j=i; i < n; j++)
{
thisSum+=a[j];
if(thisSum>maxSum)
maxSum=thisSum;
}
}
return maxSum;
}
时间复杂度
如果存在正常数c和n0使得当N≥n0时T(N)≤cf(N),则记为T(N)=O(f(N)).
法则1–FOR 循环:
一次for循环的运行时间至多是该for循环内语句(包括测试)的运行时间乘以迭代的
次数,应该取循环次数最多的情况。
法则2–嵌套的 for循环
从里向外分析这些循环。在一组嵌套循环内部的一条语句总的运行时间为该语句的运行
时间乘以该组所有的for循环的大小的乘积。
作为-一个例子,下列程序片段为O(N²):
for( i=0;i< N; i++ )
for(j=0; j < N; j++ )
k++;
法则3–顺序语句
将各个语句的运行时间求和即可(这意味着,其中的最大值就是所得的运行时间;见2.1
节的法则1(a))。
作为-一个例子,下面的程序片段先用去O(N),再花费O(N²),总的开销也是O(N²):
for(i=0;i<N;i++)
A[i]=0;
for(i=0;i<N;1++)
for( j = 0; j<N;j++ )
A[i]+=A[j]+i+j;
法则4-- IF/ELSE 语句
对于程序片段
if( Condition )
S1
else
S2
一个if/else语句的运行时间从不超过判断再加上S1和S2中运行时间长者的总的运行
时间。
除分治算法外,可将对数最常出现的规律概括为下列一般法则:如果一个算
法用常数时间(O(1))将问题的大小削减为其一部分(通常是1/2),那么该算法就是O(logN)。另一方面,如果使用常数时间只是把问题减少一个常数(如将问题减少1),那么这种算
法就是O(N)的。
例如二分查找、欧几里得算法、幂运算。
递归的时间复杂度
int maxSubSum(const int a[],int left,int right){
int maxLeftSum,maxRightSum;
int maxLeftBorderSum,maxRihtBorderSum;
int leftBorderSum,rightBorderSum;
int center,i;
if (left==right)
{
if (a[left]>0)
return a[left];
else
return 0;
}
center=(left+right)/2;
maxLeftSum=maxSubSum(a,left,center); //6
maxRightSum=maxSubSum(a,center+1,right); //7
maxLeftBorderSum=0;leftBorderSum=0;
for (i = center; i >= left; i--) //9
{
leftBorderSum+=a[i];
if (leftBorderSum>maxLeftBorderSum)
maxLeftBorderSum=leftBorderSum;
}
maxRihtBorderSum=0;rightBorderSum=0;
for (i = center+1; i <= right; i++)
{
rightBorderSum+=a[i];
if(rightBorderSum>maxRihtBorderSum)
maxRihtBorderSum=rightBorderSum;
} //19
return Max3(maxLeftSum,maxRightSum,maxLeftBorderSum+maxRihtBorderSum);
}
设花费时间为T(N).从第9行到19行只有两个非嵌套的for循环,复杂度为O(N),6、7行分别为T(N/2),所以:
T(N)=2T(N/2)+O(N)
解得T(N)=O(N*logN)
图论算法
如果在一个无向图中从每一个顶点到每个其他顶点都存在一条路径, 则称该无向图是连通的(connected)。具有这样性质的有向图称为是强连通的(stronglyconnected)。如果一个有向图不是强连通的,但是它的基础图( underlying graph),即其弧上去掉方向所形成图,是连通的,那么该有向图称为是弱连通的(weakly connected)。完全图(complete graph)是其每一对顶点间都存在一条边的图。
表示图的一种简单的方法是使用一个二维数组,称为邻接矩阵( adjacency matrix)表示
法。对于每条边(u, v),我们置A[u][v]= 1;否则,数组的元素就是0。如果边有一个权,
那么我们可以置A[u][v]等于该权,而使用一个很大或者很小的权作为标记表示不存在的
边。
如果图不是稠密的,换句话说,如果图是稀疏的(sparse),则更好的解决方法是使用邻接
表(adjacency list)表示。对每一个顶点,我们使用一个表存放所有邻接的顶点。邻接表是表示图的标准方法。
图的遍历有深度优先和广度优先算法
深度优先(DFS)
搜索方法的特点是尽可能先对纵深方向进行搜索。
LeetCode第733:
有一幅以二维整数数组表示的图画,每一个整数表示该图画的像素值大小,数值在 0 到 65535 之间。
给你一个坐标 (sr, sc) 表示图像渲染开始的像素值(行 ,列)和一个新的颜色值 newColor,让你重新上色这幅图像。
为了完成上色工作,从初始坐标开始,记录初始坐标的上下左右四个方向上像素值与初始坐标相同的相连像素点,接着再记录这四个方向上符合条件的像素点与他们对应四个方向上像素值与初始坐标相同的相连像素点,……,重复该过程。将所有有记录的像素点的颜色值改为新的颜色值。
最后返回经过上色渲染后的图像。
示例 1:
输入:
image = [[1,1,1],[1,1,0],[1,0,1]]
sr = 1, sc = 1, newColor = 2
输出: [[2,2,2],[2,2,0],[2,0,1]]
解析:
在图像的正中间,(坐标(sr,sc)=(1,1)),
在路径上所有符合条件的像素点的颜色都被更改成2。
注意,右下角的像素没有更改为2,
因为它不是在上下左右四个方向上与初始点相连的像素点。
class Solution {
static int oriColor;
public int[][] floodFill(int[][] image, int sr, int sc, int newColor) {
oriColor=image[sr][sc];
return dfs(image,sr,sc,newColor);
}
public int[][] dfs(int[][] image,int sr,int sc,int newColor){
if(sr<0||sr>=image.length||sc<0||sc>=image[0].length||image[sr][sc]==newColor||image[sr][sc]!=oriColor)
return image;
image[sr][sc]=newColor;
dfs(image,sr-1,sc,newColor);//递归地访问上面的点
dfs(image,sr+1,sc,newColor);
dfs(image,sr,sc-1,newColor);
dfs(image,sr,sc+1,newColor);
return image;
}
}
oriColor记录原始颜色,如果image[sr][sc]==newColor表示改点已访问过,可直接返回,image[sr][sc]!=oriColor表示改点与原始点不相连.
广度优先(BFS)
逐个遍历每一层相邻的.
如果用邻接表法表示的图就很容易BFS,写一个方法来遍历一个节点的所有子节点(遍历链表),从根节点开始,到最后一层结束,由于一个节点可能会有都个路径达到,所以需要标记每个节点是否被访问.
Dijkstra(迪杰斯特拉算法)
求单源最短路径.
常用贪心策略,局部最短就是全局最短.
贪心算法
贪婪算法分阶段地工作。在每一个阶段.可以认为所作决定是好的,而不考虑将来的后果。一般地说,这意味着选择的是某个局部的最优。这种“眼下能够拿到的就拿”的策略即是这类算法名称的来源。当算法终止时,我们希望局部最优就是全局最优。如果是这样的话,那么算法就是正确的;否则,算法得到的是一个次最优解(subotimal solution)。如果不要求绝对最佳答案,那么有时用简单的贪婪算法生成近似答案,而不是使用一般说来产生准确答案所需要的复杂算法。
分治算法
用于设计算法的另- -种常 用技巧为分治(divide and conquer)算法。分治算法由两部分组成:
分(divide):递归解决较小的问题(当然,基本情况除外)。
治(conquer):然后,从子问题的解构建原问题的解。
动态规划
任何数学递归公式都可以直接翻译成递归算法,但是基本现实是编译器常常不能正确对
待递归算法,结果导致低效的算法。当我们怀疑很可能是这种情况时,我们必须再给编译器提供一些帮助,将递归算法重新写成非递归算法,让后者把那些子问题的答案系统地记录在一个表内。利用这种方法的一种技巧叫做动态规划(dynamic programming)。
比如斐波那契数的输出,用递归效率比较低.线性算法用两个变量分别记录前两个的值.
int main(int argc, char const *argv[])
{
printf("%d\n", fibonacci1(8));
printf("%d\n", fibonacci2(8));
getchar();
return 0;
}
int fibonacci1(int n){//递归实现,效率低
if(n<=1) return 1;
return fibonacci1(n-1)+fibonacci1(n-1);
}
int fibonacci2(int n){//线性算法
if(n<=1) return 1;
int pre,preTwo,re=0,i;
pre=preTwo=1;
for(i=2;i<=n;i++){
re=pre+preTwo;
pre=re;
preTwo=pre;
}
return re;
}
回溯算法
回溯算法实际上一个类似枚举的搜索尝试过程,主要是在搜索尝试过程中寻找问题的解,当发现已不满足求解条件时,就“回溯”返回,尝试别的路径。回溯法是一种选优搜索法,按选优条件向前搜索,以达到目标。但当探索到某一步时,发现原先选择并不优或达不到目标,就退回一步重新选择,这种走不通就退回再走的技术为回溯法,而满足回溯条件的某个状态的点称为“回溯点”。