关于数据结构:堆排序

#include <iostream>using namespace std;#define MAXSIZE 20 typedef struct{ int key; char *otherinfo;}ElemType; typedef struct{ ElemType *r; int length; }SqList; //筛选法调整堆void HeapAdjust(SqList &L,int s,int m){ ElemType rc; int j; rc=L.r[s]; for(j=2*s;j<=m;j*=2){ if(j<m&&L.r[j].key<L.r[j+1].key) ++j; if(rc.key>=L.r[j].key) break; L.r[s]=L.r[j]; s=j; } L.r[s]=rc; } void Create_Sq(SqList &L){ int i,n; cout<<"数据个数:"; cin>>n; cout<<"待排序的数据:"; for(i=1;i<=n;i++){ cin>>L.r[i].key; L.length++; }}//建初堆void CreatHeap(SqList &L){ int i,n; n=L.length; for(i=n/2;i>0;--i) HeapAdjust(L,i,n);} //堆排序void HeapSort(SqList &L){ int i; ElemType x; CreatHeap(L); for(i=L.length;i>1;--i){ x=L.r[1]; L.r[1]=L.r[i]; L.r[i]=x; HeapAdjust(L,1,i-1); }}void show(SqList L){ int i; for(i=1;i<=L.length;i++) cout<<L.r[i].key<<" ";}int main(){ SqList L; L.r=new ElemType[MAXSIZE+1]; L.length=0; Create_Sq(L); HeapSort(L); cout<<" 堆排序 :"; show(L);} ...

November 28, 2020 · 1 min · jiezi

关于数据结构:简单选择排序

#include <iostream>using namespace std;#define MAXSIZE 20 typedef struct{ int key; char *otherinfo;}ElemType; typedef struct{ ElemType *r; int length; }SqList;//简略抉择排序 void SelectSort(SqList &L) { int i,j,k; ElemType t; for(i=1;i<L.length;++i){ k=i; for(j=i+1;j<=L.length;++j) if(L.r[j].key<L.r[k].key) k=j; if(k!=i) {t=L.r[i];L.r[i]=L.r[k];L.r[k]=t;} } } void Create_Sq(SqList &L){ int i,n; cout<<"数据个数:"; cin>>n; cout<<"待排序的数据:"; for(i=1;i<=n;i++){ cin>>L.r[i].key; L.length++; }}void show(SqList L){ int i; for(i=1;i<=L.length;i++) cout<<L.r[i].key<<" ";}int main(){ SqList L; L.r=new ElemType[MAXSIZE+1]; L.length=0; Create_Sq(L); SelectSort(L); cout<<"简略抉择排序:"; show(L);}

November 28, 2020 · 1 min · jiezi

关于数据结构:快速排序递归

#include <iostream>using namespace std;#define MAXSIZE 20 typedef struct{ int key; char *otherinfo;}ElemType; typedef struct{ ElemType *r; int length; }SqList; int Partition(SqList &L,int low,int high){ int pivotkey; L.r[0]=L.r[low]; pivotkey=L.r[low].key; while(low<high){ while(low<high&&L.r[high].key>=pivotkey) --high; L.r[low]=L.r[high]; while(low<high&&L.r[low].key<=pivotkey) ++low; L.r[high]=L.r[low]; } L.r[low]=L.r[0]; return low; }void QSort(SqList &L,int low,int high){ int pivotloc; if(low<high){ pivotloc=Partition(L,low,high); QSort(L,low,pivotloc-1); QSort(L,pivotloc+1,high); }} void QuickSort(SqList &L){ QSort(L,1,L.length);} void Create_Sq(SqList &L){ int i,n; cout<<"数据个数:"; cin>>n; cout<<"待排序的数据:"; for(i=1;i<=n;i++){ cin>>L.r[i].key; L.length++; }}void show(SqList L){ int i; for(i=1;i<=L.length;i++) cout<<L.r[i].key<<" ";}int main(){ SqList L; L.r=new ElemType[MAXSIZE+1]; L.length=0; Create_Sq(L); QuickSort(L); cout<<" 疾速排序 :"; show(L);} ...

November 27, 2020 · 1 min · jiezi

关于数据结构:希尔排序

#include <iostream>using namespace std;#define MAXSIZE 20 typedef struct{ int key; char *otherinfo;}ElemType; typedef struct{ ElemType *r; int length; }SqList; //一趟增量是dk的希尔插入排序void ShellInsert(SqList &L,int dk){ int i,j; for(i=dk+1;i<=L.length;++i) if(L.r[i].key<L.r[i-dk].key){ L.r[0]=L.r[i]; for(j=i-dk;j>0&& L.r[0].key<L.r[j].key;j-=dk) L.r[j+dk]=L.r[j]; L.r[j+dk]=L.r[0]; } }void show(SqList L){ int i; for(i=1;i<=L.length;i++) cout<<L.r[i].key<<" "; cout<<endl; }//t趟希尔排序void ShellSort(SqList &L,int dt[ ],int t){ int k; for(k=0;k<t;++k){ ShellInsert(L,dt[k]); show(L); } } void Create_Sq(SqList &L){ int i,n; cout<<"数据个数:"; cin>>n; cout<<"待排序的数据:"; for(i=1;i<=n;i++){ cin>>L.r[i].key; L.length++; }}int main(){ SqList L; L.r=new ElemType[MAXSIZE+1]; L.length=0; Create_Sq(L); int i,t; int *dt=new int[MAXSIZE]; cout<<"总趟数:"; cin>>t; for(i=0;i<t;i++){ cout<<"第"<<i+1<<"趟增量:"; cin>>dt[i]; } cout<<"希尔排序:\n"; ShellSort(L,dt,t);} ...

November 27, 2020 · 1 min · jiezi

关于数据结构:折半插入排序

#include <iostream>using namespace std;#define MAXSIZE 20 typedef struct{ int key; char *otherinfo;}ElemType; typedef struct{ ElemType *r; int length; }SqList;//折半插入排序 void BInsertSort(SqList &L){ int i,j,low,high,m; for(i=2;i<=L.length;++i){ L.r[0]=L.r[i]; low=1; high=i-1; while(low<=high){ m=(low+high)/2; if(L.r[0].key<L.r[m].key) high=m-1; else low=m+1; } for(j=i-1;j>=high+1;--j) L.r[j+1]=L.r[j]; L.r[high+1]=L.r[0]; } } void Create_Sq(SqList &L){ int i,n; cout<<"数据个数:"; cin>>n; cout<<"待排序的数据:"; for(i=1;i<=n;i++){ cin>>L.r[i].key; L.length++; }}void show(SqList L){ int i; for(i=1;i<=L.length;i++) cout<<L.r[i].key<<" ";}int main(){ SqList L; L.r=new ElemType[MAXSIZE+1]; L.length=0; Create_Sq(L); BInsertSort(L); cout<<"折半插入排序:"; show(L);} ...

November 27, 2020 · 1 min · jiezi

关于数据结构:直接插入排序

#include <iostream>using namespace std;#define MAXSIZE 20 typedef struct{ int key; char *otherinfo;}ElemType; typedef struct{ ElemType *r; int length; }SqList; //间接插入排序void InsertSort(SqList &L){ int i,j; for(i=2;i<=L.length;++i) if(L.r[i].key<L.r[i-1].key){ L.r[0]=L.r[i]; L.r[i]=L.r[i-1]; for(j=i-2; L.r[0].key<L.r[j].key;--j) L.r[j+1]=L.r[j]; L.r[j+1]=L.r[0]; } } void Create_Sq(SqList &L){ int i,n; cout<<"数据个数:"; cin>>n; cout<<"待排序的数据:"; for(i=1;i<=n;i++){ cin>>L.r[i].key; L.length++; }}void show(SqList L){ int i; for(i=1;i<=L.length;i++) cout<<L.r[i].key<<" ";}int main(){ SqList L; L.r=new ElemType[MAXSIZE+1]; L.length=0; Create_Sq(L); InsertSort(L); cout<<"间接插入排序:"; show(L);}

November 27, 2020 · 1 min · jiezi

关于数据结构:构造无向图及广度优先遍历连通图邻接矩阵非递归循环队列

#include <iostream>using namespace std;#define MVNum 100 #define MAXQSIZE 100 typedef char VerTexType; typedef int ArcType; bool visited[MVNum]; typedef struct{ VerTexType vexs[MVNum]; ArcType arcs[MVNum][MVNum]; int vexnum,arcnum; }Graph;typedef struct{ ArcType *base; int front; int rear; }sqQueue;void InitQueue(sqQueue &Q){ Q.base = new ArcType[MAXQSIZE]; if(!Q.base) exit(1); Q.front = Q.rear = 0;}void EnQueue(sqQueue &Q, ArcType e){ if((Q.rear + 1) % MAXQSIZE == Q.front) return; Q.base[Q.rear] = e; Q.rear = (Q.rear + 1) % MAXQSIZE;}bool QueueEmpty(sqQueue Q){ if(Q.rear == Q.front) return true; return false;}void DeQueue(sqQueue &Q, ArcType &u){ u = Q.base[Q.front]; Q.front = (Q.front + 1) % MAXQSIZE;} int LocateVex(Graph G , VerTexType v){ for(int i = 0; i < G.vexnum; ++i) if(G.vexs[i] == v) return i; return -1;}//创立无向图 void CreateUDG(Graph &G){ int i , j , k; cout <<"总顶点数 总边数:"; cin >> G.vexnum >> G.arcnum; for(i = 0; i < G.vexnum; ++i){ cout << "第" << (i+1) << "个点的名称:"; cin >> G.vexs[i]; } for(i = 0; i < G.vexnum; ++i) for(j = 0; j < G.vexnum; ++j) G.arcs[i][j] = 0; for(k = 0; k < G.arcnum;++k){ VerTexType v1 , v2; cout << "第" << (k + 1) << "条边附丽的顶点:"; cin >> v1 >> v2; i = LocateVex(G, v1); j = LocateVex(G, v2); G.arcs[j][i] = G.arcs[i][j] = 1; }}int FirstAdjVex(Graph G , int v){ int i; for(i = 0 ; i < G.vexnum ; ++i){ if(G.arcs[v][i] == 1 && visited[i] == false) return i; } return -1;}int NextAdjVex(Graph G , int u , int w){ int i; for(i = w ; i < G.vexnum ; ++i){ if(G.arcs[u][i] == 1 && visited[i] == false) return i; } return -1;}//广度优先遍历void BFS (Graph G, int v){ sqQueue Q; ArcType u; ArcType w; cout << G.vexs[v]; visited[v] = true; InitQueue(Q); EnQueue(Q, v); while(!QueueEmpty(Q)){ DeQueue(Q, u); for(w = FirstAdjVex(G, u); w >= 0; w = NextAdjVex(G, u, w)){ if(!visited[w]){ cout << G.vexs[w]; visited[w] = true; EnQueue(Q, w); } } }}int main(){ Graph G; CreateUDG(G); cout << "遍历无向图的起始点:"; VerTexType c; cin >> c; int i; for(i = 0 ; i < G.vexnum ; ++i){ if(c == G.vexs[i]) break; } cout << "广度优先搜寻遍历无向图:"; BFS(G , i); return 0;} ...

November 27, 2020 · 2 min · jiezi

关于数据结构:构造无向图及深度优先遍历连通图邻接矩阵递归

#include <iostream>using namespace std;#define MVNum 100 typedef char VerTexType; typedef int ArcType; typedef struct{ VerTexType vexs[MVNum]; ArcType arcs[MVNum][MVNum]; int vexnum,arcnum; }Graph;bool visited[MVNum]; int LocateVex(Graph G , VerTexType v){ for(int i = 0; i < G.vexnum; ++i) if(G.vexs[i] == v) return i; return -1;}//创立无向图 void CreateUDG(Graph &G){ int i , j , k; cout <<"总顶点数 总边数:"; cin >> G.vexnum >> G.arcnum; for(i = 0; i < G.vexnum; ++i){ cout << "第" << (i+1) << "个点的名称:"; cin >> G.vexs[i]; } for(i = 0; i < G.vexnum; ++i) for(j = 0; j < G.vexnum; ++j) G.arcs[i][j] = 0; for(k = 0; k < G.arcnum;++k){ VerTexType v1 , v2; cout << "第" << (k + 1) << "条边附丽的顶点:"; cin >> v1 >> v2; i = LocateVex(G, v1); j = LocateVex(G, v2); G.arcs[j][i] = G.arcs[i][j] = 1; }}//深度优先遍历 void DFS(Graph G, int v){ int w; cout << G.vexs[v]; visited[v] = true; for(w = 0; w < G.vexnum; w++) if((G.arcs[v][w] != 0)&& (!visited[w])) DFS(G, w); }int main(){ Graph G; CreateUDG(G); cout << "遍历无向图的起始点:"; VerTexType c; cin >> c; int i; for(i = 0 ; i < G.vexnum ; ++i){ if(c == G.vexs[i]) break; } cout << "深度优先遍历无向图:"; DFS(G , i); return 0;} ...

November 26, 2020 · 1 min · jiezi

关于数据结构:构造无向网邻接矩阵

#include <iostream>using namespace std;#define MaxInt 32767 #define MVNum 100 #define OK 1 typedef char VerTexType; typedef int ArcType; typedef struct{ VerTexType vexs[MVNum]; ArcType arcs[MVNum][MVNum]; int vexnum,arcnum; }AMGraph;int LocateVex(AMGraph G , VerTexType v){ for(int i = 0; i < G.vexnum; ++i) if(G.vexs[i] == v) return i; return -1;}//创立无向网int CreateUDN(AMGraph &G){ int i , j , k; cout <<"总顶点数 总边数:"; cin >> G.vexnum >> G.arcnum; for(i = 0; i < G.vexnum; ++i){ cout << "第" << (i+1) << "个点的名称:"; cin >> G.vexs[i]; } for(i = 0; i < G.vexnum; ++i) for(j = 0; j < G.vexnum; ++j) G.arcs[i][j] = MaxInt; for(k = 0; k < G.arcnum;++k){ VerTexType v1 , v2; ArcType w; cout << "第" << (k + 1) << "条边附丽的顶点及权值:"; cin >> v1 >> v2 >> w; i = LocateVex(G, v1); j = LocateVex(G, v2); G.arcs[i][j] = w; G.arcs[j][i] = G.arcs[i][j]; } return OK; }int main(){ AMGraph G; int i , j; CreateUDN(G); for(i = 0 ; i < G.vexnum ; ++i){ for(j = 0; j < G.vexnum; ++j){ if(j != G.vexnum - 1){ if(G.arcs[i][j] != MaxInt) cout << G.arcs[i][j] << "\t"; else cout << "∞" << "\t"; } else{ if(G.arcs[i][j] != MaxInt) cout << G.arcs[i][j] <<endl; else cout << "∞" <<endl; } } } return 0;}` ...

November 26, 2020 · 1 min · jiezi

关于数据结构:构建赫夫曼树及求赫夫曼编码从叶子到根逆向

#include<iostream>#include<string.h>using namespace std;typedef struct{ int weight; int parent,lchild,rchild;}HTNode,*HuffmanTree;void Select(HuffmanTree HT,int len,int &s1,int &s2){ int i,min1=0x3f3f3f3f,min2=0x3f3f3f3f; for(i=1;i<=len;i++){ if(HT[i].weight<min1&&HT[i].parent==0){ min1=HT[i].weight; s1=i; } } int temp=HT[s1].weight; HT[s1].weight=0x3f3f3f3f; for(i=1;i<=len;i++){ if(HT[i].weight<min2&&HT[i].parent==0){ min2=HT[i].weight; s2=i; } } HT[s1].weight=temp;}//结构赫夫曼树void CreatHuffmanTree(HuffmanTree &HT,int n){ int m,s1,s2,i; if(n<=1) return; m=2*n-1; HT=new HTNode[m+1]; for(i=1;i<=m;++i){ HT[i].parent=0; HT[i].lchild=0; HT[i].rchild=0; } cout<<"叶子结点权值:"; for(i=1;i<=n;++i) cin>>HT[i].weight; for(i=n+1;i<=m;++i){ Select(HT,i-1,s1,s2); HT[s1].parent=i; HT[s2].parent=i; HT[i].lchild=s1; HT[i].rchild=s2 ; HT[i].weight=HT[s1].weight+HT[s2].weight; } } typedef char **HuffmanCode; //赫夫曼编码 void CreatHuffmanCode(HuffmanTree HT,HuffmanCode &HC,int n){ int i,start,c,f; HC=new char*[n+1]; char *cd=new char[n]; cd[n-1]='\0'; for(i=1;i<=n;++i){ start=n-1; c=i; f=HT[i].parent; while(f!=0){ --start; if(HT[f].lchild==c) cd[start]='0'; else cd[start]='1'; c=f; f=HT[f].parent; } HC[i]=new char[n-start]; strcpy(HC[i], &cd[start]); } delete cd; } void show(HuffmanTree HT,HuffmanCode HC,int n){ for(int i=1;i<=n;i++) cout<<HT[i].weight<<"赫夫曼编码为:"<<HC[i]<<endl;} int main(){ HuffmanTree HT; int n; cout<<"叶子结点个数:"; cin>>n; CreatHuffmanTree(HT,n); HuffmanCode HC; CreatHuffmanCode(HT,HC,n); show(HT,HC,n);} ...

November 25, 2020 · 1 min · jiezi

关于数据结构:先序建立二叉链表及先序中序遍历链栈非递归

#include<iostream>using namespace std;typedef struct BiNode{ char data; struct BiNode *lchild,*rchild;}BiTNode,*BiTree;//先序建设二叉链表void CreateBiTree(BiTree &T){ char ch; cin >> ch; if(ch=='#') T=NULL; else{ T=new BiTNode; T->data=ch; CreateBiTree(T->lchild); CreateBiTree(T->rchild); }}//链栈typedef struct StackNode{ BiTNode data; struct StackNode *next;}StackNode,*LinkStack;//结构空栈Svoid InitStack(LinkStack &S){ S=NULL;}bool StackEmpty(LinkStack S){ if(!S) return true; return false;}//入栈 void Push(LinkStack &S,BiTree e){ StackNode *p=new StackNode; p->data=*e; p->next=S; S=p;}//出栈 void Pop(LinkStack &S,BiTree e){ if(S!=NULL){ *e=S->data; StackNode *p=S; S=S->next; delete p; }} //中序遍历 void In(BiTree T){ LinkStack S; BiTree p; BiTree q=new BiTNode; InitStack(S); p=T; while(p||!StackEmpty(S)){ if(p){ Push(S,p); p=p->lchild; } else{ Pop(S,q); cout<<q->data; p=q->rchild; } } } //先序遍历 void Pre(BiTree T){ LinkStack S; BiTree p; BiTree q=new BiTNode; InitStack(S); p=T; while(p||!StackEmpty(S)){ if(p){ Push(S,p); cout<<p->data; p=p->lchild; } else{ Pop(S,q); p=q->rchild; } } }int main(){ BiTree tree; cout<<"先序建设二叉链表:"; CreateBiTree(tree); cout<<"先序遍历的后果为:"; Pre(tree); cout<<endl; cout<<"中序遍历的后果为:"; In(tree); cout<<endl;} ...

November 25, 2020 · 1 min · jiezi

关于数据结构:先序建立二叉链表及先序中序后序遍历递归

#include<iostream>using namespace std;typedef struct BiNode{ char data; struct BiNode *lchild,*rchild;}BiTNode,*BiTree;//先序建设二叉链表void CreateBiTree(BiTree &T){ char ch; cin >> ch; if(ch=='#') T=NULL; else{ T=new BiTNode; T->data=ch; CreateBiTree(T->lchild); CreateBiTree(T->rchild); }}//先序遍历 void Pre(BiTree T){ if(T){ cout<<T->data; Pre(T->lchild); Pre(T->rchild); }}//中序遍历 void In(BiTree T){ if(T){ In(T->lchild); cout<<T->data; In(T->rchild); }}//后序遍历 void Post(BiTree T){ if(T){ Post(T->lchild); Post(T->rchild); cout<<T->data; }}int main(){ BiTree tree; cout<<"先序建设二叉链表:"; CreateBiTree(tree); cout<<"先序遍历的后果为:"; Pre(tree); cout<<endl; cout<<"中序遍历的后果为:"; In(tree); cout<<endl; cout<<"后序遍历的后果为:"; Post(tree); cout<<endl;}

November 24, 2020 · 1 min · jiezi

关于数据结构:线程池浅谈

线程池概念线程池就是首先创立一些线程,它们的汇合称为线程池。应用线程池能够很好的进步性能,线程池在系统启动时创立大量闲暇的线程,当工作池有工作时,线程池中的线程排队去支付工作池的工作,如果以后没有工作就阻塞期待。 工作机制在有线程池的状况下,工作是交给线程池而不是间接交给某个线程,在线程池拿到工作后,寻找是否有闲暇的线程,如果有就交付给该线程执行。一个线程,只能同时执行一个工作,执行结束后,线程重回闲暇状态,期待下一个工作。应用线程池的起因多线程运行工夫,零碎一直启动和敞开线程,老本很高,会适度耗费线程资源,以及适度切换线程的危险,从而导致系统资源的解体。 图解线程池 代码线程池构造typedef struct NMANAGER{ struct NWORKER *workers; // 治理的线程链表头节点 struct NJOBS *jobs; //工作池头节点 int totWorkers; // 以后一共领有多少线程 int idleWorkers; // 以后闲暇线程数量 pthread_mutex_t pool_mtx; // 线程池互斥锁 pthread_cond_t pool_cond; // 线程池条件锁,所有线程期待这个条件锁} nManager;线程构造typedef struct NWORKER{ pthread_t pthreadid; //该线程的线程id struct NMANAGER *pool; // 处于哪一个线程池 int terminate; // 是否被终止,1为销毁该线程,0则相同 struct NWORKER *next; //链表中的下一个线程 struct NWORKER *prev;//链表中的上一个线程} nWorker;工作池typedef struct NJOBS{ void (*func)(void *arg); //工作回调,线程执行该工作 void *user_data; // 工作的参数 struct NJOBS *next; //工作池的下一个工作 struct NJOBS *prev; // 工作池上一个工作} nJobs;线程池的创立int ThreadPoolCreate(threadpool *pool, int numsThread){ //1. 初始化线程池相干属性 //2. 创立肯定数量线程,扔进线程池 if (pool == NULL) { printf("create error\n"); return 1; } if (numsThread <= 0) numsThread = 1; memset(pool, 0, sizeof(nManager)); pool->totWorkers = 0; pool->idleWorkers = 0; pthread_mutex_init(&pool->pool_mtx, NULL); pthread_cond_init(&pool->pool_cond, NULL); //pool->pool_mtx = PTHREAD_MUTEX_INITIALIZER; pthread_t managerpid; pthread_create(&managerpid, NULL, ManagerThread, (void *)pool); pthread_detach(managerpid); int i = 0; for (i = 0; i < numsThread; i++) { nWorker *worker = (nWorker *)malloc(sizeof(nWorker)); if (worker == NULL) { printf("worker error\n"); return 1; } memset(worker, 0, sizeof(nWorker)); int ret = pthread_create(&worker->pthreadid, NULL, ThreadWorking, worker); if (ret) { perror("pthread create"); return 1; } pthread_detach(worker->pthreadid); worker->terminate = 0; worker->pool = pool; LIST_ADD(worker, pool->workers); // 扔进线程池 pthread_mutex_lock(&pool->pool_mtx); pool->totWorkers++; // 总共线程减少 pool->idleWorkers++; // 闲暇线程 pthread_mutex_unlock(&pool->pool_mtx); } return 0;}销毁线程池void ThreadPoolDestroy(threadpool *pool){ nWorker *w = pool->workers; for (w = pool->workers; w != NULL; w = w->next) { w->terminate = 1; // 令teminate = 1 ,在线程的执行函数中,会本人把本人给覆灭,耦合度低。 } pthread_mutex_lock(&pool->pool_mtx); pthread_cond_broadcast(&pool->pool_cond); // 告诉所有期待条件锁的线程,没有期待的线程执行完工作之后,会自行销毁 pthread_mutex_unlock(&pool->pool_mtx); free(pool);}线程池增加工作void ThreadPushJob(threadpool *pool, nJobs *job){ pthread_mutex_lock(&pool->pool_mtx); LIST_ADD(job, pool->jobs); // 向线程池增加工作 printf("add jobs in pool\n"); pthread_cond_signal(&pool->pool_cond); // 告诉一个线程来工作了 pthread_mutex_unlock(&pool->pool_mtx);}线程执行函数void *ThreadWorking(void *arg){ nWorker *worker = (nWorker *)arg; while (1) { pthread_mutex_lock(&worker->pool->pool_mtx); while (worker->pool->jobs == NULL) { printf("work waiting------\n"); if (worker->terminate == 1) { //该线程被销毁 break; } pthread_cond_wait(&worker->pool->pool_cond, &worker->pool->pool_mtx); //等到工作池有工作 , //在这个函数之前解锁,而后阻塞在该条件变量上,该函数之后加上相应的互斥锁 } if (worker->terminate == 1) { pthread_mutex_unlock(&worker->pool->pool_mtx); break; } nJobs *jobs = worker->pool->jobs; LIST_DEL(jobs, worker->pool->jobs); //把该工作从工作池取下来 pthread_mutex_unlock(&worker->pool->pool_mtx); // 当初容许其余线程来工作池取工作啦 pthread_mutex_lock(&worker->pool->pool_mtx); worker->pool->idleWorkers--; // 闲暇线程减一 pthread_mutex_unlock(&worker->pool->pool_mtx); printf("working -------\n"); jobs->func(jobs->user_data); //执行该工作的回调函数 pthread_mutex_lock(&worker->pool->pool_mtx); worker->pool->idleWorkers++; //闲暇线程加一 pthread_mutex_unlock(&worker->pool->pool_mtx); free(jobs); // 开释该工作内存 } // 销毁该线程,同时批改与该线程相干的信息 pthread_mutex_lock(&worker->pool->pool_mtx); worker->pool->totWorkers--; worker->pool->idleWorkers--; LIST_DEL(worker, worker->pool->workers); // 从线程池中取出 pthread_mutex_unlock(&worker->pool->pool_mtx); free(worker); // 开释 pthread_exit(NULL);}剩下问题目前线程池不反对,动静的放大和扩张。个别闲暇线程过多的时候,咱们思考缩小线程池容量,忙线程多的时候,咱们思考减少线程。在线程池的构造体中曾经有了总共线程个数和休闲线程个数,能够应用这个两个进行编写相应程序。线程池大小问题,咱们能够应用一个线程:管理者线程来保护线程池的大小.

November 24, 2020 · 2 min · jiezi

关于数据结构:二叉树遍历算法的改进非递归实现

二叉树遍历算法的改良二叉树的深度优先遍历算法都是用递归函数实现的,这是很低效的,起因在于零碎帮你调用了一个栈并做了诸如爱护现场和复原现场等简单的操作,才使得遍历能够用十分简洁的代码实现。二叉树深度优先遍历算法的非递归实现用用户定义的栈来代替零碎栈,也就是用非递归的形式来实现遍历算法,能够失去不小的效率晋升。 二叉树深度优先遍历算法的非递归实现(1)先序遍历非递归算法要写出其遍历的非递归算法,其次要工作是用本人定义的栈来代替零碎栈的性能。 以图1所示的二叉树为例,过程为图二所示 初态栈空 结点1入栈。出栈,输入栈顶结点1,并将1的左、右孩子结点(2和4)入栈;右孩子先入栈,左孩子后入栈,因为对左孩子的拜访要先于右孩子,后入栈的会先出栈拜访。出栈,输入栈顶结点2,并将2的左、右孩子结点(3和5)入栈。出栈,输入栈顶结点3,3为叶子结点,无孩子,本步无结点入栈。出栈,输入栈顶结点5。出栈,输入栈顶结点4,此时栈空,进入终态。 遍历序列为1,2,3,5,4。 代码如下 void preorderNonrecursion(BTNode *bt){ if(bt != NULL) { BTNode *Stack[maxSize]; //定义一个栈 int top = -1; //初始化栈 BTNode *p; Stack[++top] = bt; //根结点入栈 while(top != -1) //栈空循环完结 { p = Stack[top--]; //出栈并输入栈顶结点 Visit(p); //Visit()为拜访p的函数 if(p->rchild != NULL) //栈顶结点的右孩子存在,则右孩子入栈 Stack[++top] = p->rchild; if(p->lchild != NULL) //栈顶结点的左孩子存在,则左孩子入栈 Stack[++top] = p->lchild; } }}(2)中序遍历非递归算法相似于先序遍历,对图1中的二叉树进行中序遍历,各个结点进栈、出栈过程如图3所示。 初态栈空。 结点1入栈,1左孩子存在。结点2入栈,2左孩子存在。结点3入栈,3左孩子不存在。出栈,输入栈顶结点3,3右孩子不存在。出栈,输入栈顶结点2,2右孩子存在,右孩子5入栈,5左孩子不存在。出栈,输入栈顶结点5,5右孩子不存在。出栈,输入栈顶结点1,1右孩子存在,右孩子4入栈,4左孩子不存在。出栈,输入栈顶结点4,此时栈空,进入终态。 遍历序列为3,2,5,1,4。 由以上步骤能够看出,中序非递归遍历过程如下: 开始根结点入栈循环执行如下操作:如果栈顶结点左孩子存在,则左孩子进栈;如果栈顶结点左孩子不存在,则出栈并输入栈顶结点,而后查看其右孩子是否存在,如果存在,则右孩子入栈。当栈空时算法完结。代码如下 void inorderNonrecursion(BTNode *bt){ if(bt != NULL) { BTNode *Stack[maxSize]; int top = -1; BTNode *p; p = bt; /*下边循环实现中序遍历。留神:图3进栈、出栈过程在7中会呈现栈空状态,然而这时遍历还没有完结,因根结点的右子树还没有遍历,此时p非空,依据这一点来维持循环的进行*/ while(top != -1 || p != NULL) { while(p != NULL) //左孩子存在,则左孩子入栈 { Stack[++top] = p; p = p->lchild; } if(top != -1) //在栈不空的状况下出栈并输入出栈结点 { p = Stack[top--]; Visit(p); p = p->rchild; } } }}(3)后序遍历非递归算法首先写出图1中二叉树进行先序遍历和后序遍历的序列。 ...

November 23, 2020 · 1 min · jiezi

关于数据结构:LeetCode069x的平方根easy

标签:二分 题目:x的平方根 题号:69 题干:实现 int sqrt(int x) 函数。计算并返回 x 的平方根,其中 x 是非负整数。因为返回类型是整数,后果只保留整数的局部,小数局部将被舍去 示例1: 输出: 4输入: 2 示例2: 输出: 8 输入: 2 解释: 8的平方根是 2.82842..., 因为返回类型是整数,小数局部将被舍去 原题比较简单,因而这里做简略的变形,求x的平方根,要求能够获取指定精度的后果 示例:输出:8 0.000001输入:2.828427阐明:0.000001示意准确到小数点后六位思路: 要求x的平方根,最容易想到的就是从1到x,一个一个的试,这种办法的工夫复杂度是O(n),这不是咱们想要的二分查找算法,显然是用来查找的算法,咱们的目标也算是查找。如果你看过我的上一篇二分查找的文章,里边有说到什么状况下会想到用二分查找,比方咱们要查找的一系列数是有序的等,咱们要求x的平方根,那必定在[1, x]这个区间找(x>1),它显然是有序的。这天然 会想到用二分来查找应用二分查找算法,须要两个游标low和high。须要思考他们的初始值是什么?以及当mid不满足时,low和hight如何变动?既然是求x的平方根,x的值有两种状况。第一种是x<1,这种状况,咱们要求的后果必定在[0, 1]这区间内,所以就能够初始化low = 0,hight=1,那mid就为(low+hight)/2x的第二种状况就很简略了,当x>1时,它的平方根的值必定在[1, n]区间内,所以low = 1, high=x,mid = (low+hight)/2不难想到,当x-mid*mid < 0时,阐明mid太大,那此时咱们就应该在[1, mid]之间找,即,此时让hight=mid-1如果x-midmid > 0,这时要思考x-midmid是否大于咱们要求的精度,如果大于这个精度,那此时low = mid+1有了以上这些条件,基本上这道题就进去了。如果你依照上边的思路把代码写进去之后会发现是有问题的,当x<1的时候程序就不能失常运行了。起因是low和high变动的时候,如果x<1的时候,后果必定是小于1的,如果间接让high或low为mid加1或减1必定就不对了因而,当x-mid*mid < 0时,应该让high=(mid+high)/2代码实现 func SolutionSqrt(num float64, precision float64) float64 { if num < 0 { fmt.Println("参数不非法") return -1.000 } var low,high float64 if num > 1 { low = 1 high = num } else { low = num high = 1 } return Sqrt(num, low, high, precision)}func Sqrt(num float64, low float64, high float64, precision float64) float64 { mid := (low+high) / 2 if num - (mid * mid) < 0 { return Sqrt(num, low, (mid+high)/2, precision) } else { if num - (mid * mid) > precision { return Sqrt(num, (low + mid)/2, high, precision) } return mid }}

November 23, 2020 · 1 min · jiezi

关于数据结构:二叉树遍历算法递归实现层次遍历

二叉树遍历算法二叉树的存储构造typedef struct BTNode{ char data; //这里默认结点data为char型 struct BTNode *lchild; struct BTNode *rchild;}BTNode;二叉树的遍历算法1 先序遍历先序遍历的操作如下。 如果二叉树为空树,则什么都不做;否则: 1)拜访根节点 2)先序遍历左子树 3)先序遍历右子树 形容如下: void preorder(BTNode *p){ if(p != NULL) { visit(p); //假如拜访函数Visit()曾经定义过 preorder(p->lchild); //先序遍历左子树 preorder(p->rchild); //先序遍历右子树 }}2 中序遍历中序遍历的操作如下。 如果二叉树为空树,则什么都不做;否则: 1)中序遍历左子树 2)拜访根节点 3)中序遍历右子树 形容如下: void inorder(BTNode *p){ if(p != NULL) { inorder(p->lchild); visit(p); inorder(p->rchild); }}3 后序遍历后序遍历的操作如下。 如果二叉树为空树,则什么都不做;否则: 1)后序遍历左子树 2)后序遍历右子树 3)拜访根节点 形容如下: void postorder(BTNode *p){ if(p != NULL) { postorder(p->lchild); postorder(p->rchild); visit(p); }}4 档次遍历<img src="https://irabbit756.oss-cn-shanghai.aliyuncs.com/数据结构/二叉树遍历/1二叉树档次遍历.png" style="zoom: 67%;" /> ...

November 20, 2020 · 1 min · jiezi

关于数据结构:KMP算法及其改进算法

字符贮存在1~length的地位上 简略模式匹配 思路:从主串的第一个地位起和模式串的第一个字符开始比拟,如果相等,则持续逐个比拟后续字符;否则从主串的第二个字符开始,再从新用上一步的办法与模式串中的字符做比拟,以此类推,直到比拟完模式串中的所有字符。若匹配胜利,则返回模式串在主串中的地位;若匹配不胜利,则返回一个可区别于主串所有地位的标记,如“0”。 int index(Str str,Str substr){ int i = 1,j = 1,k = 1;//串从数组下标1地位开始存储,初值为1 while(i <= str.length && j <= substr.length) { if(str.ch[i] == substr[j]) { i++; j++; } else { j = 1; i = ++k;//匹配失败,i从主串的下一个地位开始匹配,k贮存了主串上一次的起始地位 } } if(j > substr.length) return k; else ruturn 0;}主串(ABABCABCACBAB)和模式串(ABCAC)匹配过程 KMP算法 设主串为s1s2...sn,模式串为p1p2...pm,在下面的匹配过程中,经常出现一个要害状态(表2),其中i和j别离为主串和模式串中以后参加比拟的两个字符的下标。 模式串的前部某子串P1P2...Pj-1与主串中的一个子串Si-j+1Si-j+2...Si-1匹配,而Pj与Si不匹配。每当呈现这种状态时,简略模式匹配算法的做法是:一律将i赋值为i-j+2,j赋值为1,从新开始比拟。这个过程反映到表2中能够形象地示意为模式串先向后挪动一个地位,而后从第一个字符P1开始一一和以后主串中对应的字符做比拟;当再次发现不匹配时,反复上述过程。这样做的目标是试图打消Si处的不匹配,进而开始Si+1及其当前字符的比拟,使得整个过程得以推动上来。 如果在模式串后移的过程中又呈现了其前部某子串P1P2…与主串中某子串…Si-2Si-1相匹配的状态,则认为这是一个提高的状态。因为通过模式串后移排除了一些不可能匹配的状态,来到了一个新的部分匹配状态,并且此时Si有了和模式串中对应字符匹配的可能性。为了不便表述,记表中形容的状态为Sk,此处的新状态为Sk+1,此时能够将简略模式匹配过程看成一个由Sk向Sk+1推动的过程。当由Sk来到Sk+1时有两种状况可能产生:其一,S处的不匹配被解决,从si+1持续往下比拟,若来到新的不匹配字符地位,则模式串后移寻找状态Sk+2;其二,Si处的不匹配依然存在,则模式串持续后移寻找状态Sk+2如此进行上来,直到失去最终后果。 阐明:为了使上边其一与其二的表述看起来清晰工整且抓住重点,此处省略了对匹配胜利与失败这两种容易了解的状况的形容。 阐明:模式串后移使P1挪动到Si+1,即模式串整个移过Si的状况也认为是Si处的不匹配被解决。试想,如果在匹配过程中能够省略掉模式串逐步后移的过程,而从Sk间接跳到Sk+1,则能够大大提高匹配效率。带着这个想法,咱们把Sk+1状态增加到表2中失去表3。 察看发现,P1P2...Pj-1和Si-j+1Si-j+2...Si-1是完全相同的,且咱们钻研的是从Sk跳到Sk+1,因而,删除表3对于主串的一行,失去表4。 由表4可知,P1P2...Pt-1和Pj-t+1Pj-t+2...Pj-1匹配。记P1P2..Pj-1为F,记P1P2...Pt-1为FL,记Pj-t+1Pj-t+2...Pj-1为FR。所以,只需将F后移到使得FL和FR重合的地位(上表有色部位),即可实现Sk间接跳到Sk+1。 ...

November 20, 2020 · 2 min · jiezi

关于数据结构:你以为只是简单的排序二

上一篇文章中分享了冒泡排序、插入排序、抉择排序这三种排序算法,它们的工夫复杂度都是O(n^2),比拟高,适宜小规模数据的排序。这篇文章,分享两种工夫复杂度为O(nlogn)的排序算法,归并排序和疾速排序。这两种排序算法适宜大规模的数据排序,更加的罕用一些 归并排序归并排序思维归并排序核心思想:将待排序的数据分成前后两个局部,而后对前后两个局部的数据别离进行排序,再将前后两局部合并,失去的后果就是排好序的数据了 语言很形象,看图 归并排序应用的就是分治思维。分治,顾名思义,就是分而治之,将一个大问题分解成小的子问题来解决。小的子问题解决了,大问题也就解决了 分治思维跟之前的一篇文章中的递归很像,然而,分治是一种解决问题的解决思维,递归是一种编程技巧。归并排序用的是分治思维,能够用递归来实现 归并排序实现如果你看过上一篇递归的文章,上边有介绍到,解递归问题,要剖析出递归公式,而后找到递归终止条件,有了这两个,基本上递归代码就进去了。有了上边的那个图,基本上能够失去下边的递推公式 递归公式MergeSort(start, end) = merge(MergeSort(start,(start+end)/2), MergeSort((start+end)/2 + 1, end))递归终止条件start >= endMergeSort(start, end)示意的是给下标start到end之间的数组中的数据进行排序,将这个这个排序分成了两个子问题,一个是MergeSort(start,(start+end)/2),另一个是MergeSort((start+end)/2 + 1), end),当把这两个子问题排好序之后,再将它们合并,就把下标start到end之间的数据排好序了 最外层的那个merge就是对两个曾经有序的数组进行合并,具体过程如下:用两个游标i和j,别离指向两个待merge数组(arr)的第一个元素(这两个数组各自是有序的),比拟游标对应地位元素的大小。如果arr[i]<arr[j],则将arr[i]放入到一个新的数组中tmp,而后i向后挪动一位,否则,将a[j]放入tmp中,j向后一位。反复以上过程,直到有一个子数组空了,而后把另一个子数组中的数据追加到tmp的尾部,以上过程,即可将两个别离有序的数组合并成一个有序数组。具体过程如图: 代码实现 func MergeSort(arr []int) []int { if len(arr) <= 0 { fmt.Println("参数不非法") return nil } //递归终止条件,当拆分后的数组长度少于两个元素的时候 if len(arr) < 2 { return arr } midIndex := len(arr) / 2 leftArr := MergeSort(arr[0:midIndex]) rightArr := MergeSort(arr[midIndex:]) result := merge(leftArr, rightArr) return result}func merge(leftArr, rightArr []int) []int { var mergeRes []int leftIndex, rightIndex := 0, 0 leftLength, rightLength := len(leftArr), len(rightArr) for leftIndex < leftLength && rightIndex < rightLength { if leftArr[leftIndex] > rightArr[rightIndex] { mergeRes = append(mergeRes, rightArr[rightIndex]) rightIndex++ } else { mergeRes = append(mergeRes, leftArr[leftIndex]) leftIndex++ } } if leftIndex == leftLength{ for rightIndex < rightLength { mergeRes = append(mergeRes, rightArr[rightIndex]) rightIndex++ } } if rightIndex == rightLength { for leftIndex < leftLength { mergeRes = append(mergeRes, leftArr[leftIndex]) leftIndex++ } } return mergeRes}归并排序性能剖析首先归并排序是稳固排序算法,这个次要取决于merge操作,也就是将两个有序数组合并成一个有序数组。在合并的过程中,当两个数组中有雷同的元素时,先把前边那局部数组中元素放入到tmp中,这样就能够保障雷同的元素,在合并前后程序不变 ...

November 17, 2020 · 2 min · jiezi

关于数据结构:8-数据结构PHP实现-线段树的实现

1. 特色不肯定是齐全二叉树肯定是温和二叉树叶子结点存储的是理论的值,非叶子结点存的是自定义的内容2. 工夫复杂度操作工夫复杂度查问O(logn)3. 线段树的图解 4. 代码<?php/** * content: 线段树(区间树) * create: 2020-11-12 */namespace HeapBundle;use ArrayBundle\BaseArray;class SegmentTreeHeap{ /** * 传入的数组对象 * @var BaseArray */ protected $array; /** * 数组 * @var array */ protected $tree = []; public function __construct(BaseArray $array) { $this->array = $array; $this->build(0, 0, $this->array->getSize() - 1); } /** * 构建线段树 * @param int $treeIndex * @param int $min * @param int $max * @throws \Exception */ public function build(int $treeIndex, int $min, int $max) { // 如果线段区间的最小值和最小值雷同,则示意为叶子结点 if ($min == $max) { $this->tree[$treeIndex] = $this->array->get($max); return; } // 四舍五入取两头值 最大值减最小值而后除以2拿到两头值,并加上最小值 $mid = floor(($max - $min) / 2) + $min; // 获取左儿子的索引值,并递归往下构建 $leftIndex = $this->leftChildIndex($treeIndex); $this->build($leftIndex, $min, $mid); // 获取右儿子的索引值,并递归往下构建 $rightIndex = $this->rightChildIndex($treeIndex); $this->build($rightIndex, $mid + 1, $max); // 非叶子结点的值保留的是它上面所有结点的相加值, 这里能够改为它上面结点的总和值 $this->tree[$treeIndex] = $this->tree[$leftIndex] . '+' . $this->tree[$rightIndex]; } /** * 打印线段树 */ public function varDump() { ksort($this->tree); print_r($this->tree); } /** * 获取线段树的长度 * @return int */ public function getSize(): int { return count($this->tree); } /** * 获取左儿子索引 * @param int $parentIndex * @return int * @throws \Exception */ public function leftChildIndex(int $parentIndex): int { if ($parentIndex < 0) throw new \Exception('父结点的索引不能小于0'); return $parentIndex * 2 + 1; } /** * 获取右儿子索引 * @param int $parentIndex * @return int * @throws \Exception */ public function rightChildIndex(int $parentIndex): int { if ($parentIndex < 0) throw new \Exception('父结点的索引不能小于0'); return $parentIndex * 2 + 2; }}5.示例<?phprequire_once __DIR__ . '/../../vendor/autoload.php';$array = new ArrayBundleBaseArray();for ($i = 0; $i < 10; $i++) { $array->addLast($i + 10);}$heap = new HeapBundleSegmentTreeHeap($array);$heap->varDump();Array( [0] => 10+11+12+13+14+15+16+17+18+19 [1] => 10+11+12+13+14 [2] => 15+16+17+18+19 [3] => 10+11+12 [4] => 13+14 [5] => 15+16+17 [6] => 18+19 [7] => 10+11 [8] => 12 [9] => 13 [10] => 14 [11] => 15+16 [12] => 17 [13] => 18 [14] => 19 [15] => 10 [16] => 11 [23] => 15 [24] => 16)

November 15, 2020 · 2 min · jiezi

关于数据结构:数据结构与算法-C-最大小堆的插入与删除

用数组实现最大/小堆,有两种形式,一种是Array[0]不存理论值,另一种是Array[0]存理论值。第一种的话,数组长度比堆结点数多1,因为[0]是没有存任何结点的,对于i的左右子结点为2i和2i + 1,父节点为(i - 1)/2;第二种的话,数组长度等于堆结点数,因为[0]是根结点的,对于i的左右子结点为2i + 1和2i + 2,父节点为i/2。这里以最大堆为例; C语言实现第一种:#include <stdio.h>#include <stdlib.h>typedef struct MaxHeap { int *data; //数据域 int size; //长度 int max;//最大长度} MaxHeap;//插入新结点void insert(struct MaxHeap *p, int item){ if (p->size == p->max) { printf(":\n"); return; } int i = ++p->size;//先加1再复制给int,这样第一个插入的数组下标就是1 for (; p->data[i/2] < item; i/=2)//上浮 { if (i <= 1) { break; } p->data[i] = p->data[i/2]; } p->data[i] = item;}//删除根节点,void delete(struct MaxHeap *p){ p->data[1] = p->data[p->size--]; int i = 1; int tmp; while(2*i <= p->size){ //如果有右节点 if (2*i + 1 <= p->size) { if (p->data[2*i + 1] > p->data[i] || p->data[2*i] > p->data[i]) { if (p->data[2*i + 1] > p->data[2*i]) { tmp = p->data[i]; p->data[i] = p->data[2*i + 1]; p->data[2*i + 1] = tmp; i = 2*i + 1; }else{ tmp = p->data[i]; p->data[i] = p->data[2*i]; p->data[2*i] = tmp; i = 2*i; } }else{ return; } }else{ //只有左节点 if (p->data[2*i] > p->data[i]) { tmp = p->data[i]; p->data[i] = p->data[2*i]; p->data[2*i] = tmp; i = 2*i; }else{ return; } } }}//遍历堆void TraverseArray(struct MaxHeap *p){ if (p != NULL) { printf("size:%d \n",p->size); for (int i = 1; i <= p->size; ++i) { printf("%d \n",p->data[i]); } }}//校验堆void checkMaxHeap(struct MaxHeap *p){ if (p != NULL) { for (int i = 1; i <= p->size; ++i) { if (2*i <= p->size) { if (p->data[i] < p->data[2*i]) { printf("error l %d \n",i); } } if (2*i +1 <= p->size) { if (p->data[i] < p->data[2*i +1]) { printf("error r %d \n",i); for (int j = 0; j <= 2*i +1; ++j) { printf("error r: %d \n",p->data[j]); } return; } } } }}C语言实现第二种:#include <stdio.h>#include <stdlib.h>typedef struct MaxHeap { int *data; //数据域 int size; //长度 int max;//最大长度} MaxHeap;void TraverseArray(struct MaxHeap *p){ if (p != NULL) { printf("size:%d \n",p->size); for (int i = 0; i < p->size; ++i) { printf("%d \n",p->data[i]); } }}void insert(struct MaxHeap *p, int item){ if (p->size == p->max) { printf("******:\n"); return; } int i = p->size++; for (; i > 0 && p->data[(i-1)/2] < item; i=(i-1)/2) { if (i < 1) { break; } p->data[i] = p->data[(i-1)/2]; } p->data[i] = item;}void delete(struct MaxHeap *p){ p->data[0] = p->data[--p->size]; int i = 0; int tmp = 0; while(2*i + 1 < p->size){ //如果有右节点 if (2*i + 2 < p->size) { if (p->data[2*i + 2] > p->data[i] || p->data[2*i + 1] > p->data[i]) { if (p->data[2*i + 2] > p->data[2*i + 1]) { printf("dele r max :%d \n",i); tmp = p->data[i]; p->data[i] = p->data[2*i + 2]; p->data[2*i + 2] = tmp; i = 2*i + 2; }else{ printf("dele l max :%d \n",i); tmp = p->data[i]; p->data[i] = p->data[2*i + 1]; p->data[2*i + 1] = tmp; i = 2*i + 1; } }else{ return; } }else{ //只有左节点 if (p->data[2*i + 1] > p->data[i]) { tmp = p->data[i]; p->data[i] = p->data[2*i + 1]; p->data[2*i + 1] = tmp; i = 2*i + 1; }else{ return; } } }}void checkMaxHeap(struct MaxHeap *p){ if (p != NULL) { for (int i = 1; i <= p->size; ++i) { if (2*i <= p->size) { if (p->data[i] < p->data[2*i + 1]) { printf("error l %d \n",i); } } if (2*i +1 <= p->size) { if (p->data[i] < p->data[2*i +1]) { printf("error r %d \n",i); for (int j = 0; j <= 2*i +1; ++j) { printf("error r: %d \n",p->data[j]); } return; } } } }}

November 5, 2020 · 3 min · jiezi

关于数据结构:特殊的数据结构布隆过滤器的原理和实现及探究

一、布隆过滤器的应用价值有时候咱们须要判断一个元素是否在一个汇合中。比方,在字处理软件中,须要查看一个单词是否拼写正确(也就是要判断它是否在已知的字典里);在警察零碎中,一个嫌疑人的名字是否呈现在嫌疑名单上;在网络爬虫里,一个网址是否曾经被拜访过,等等。 最间接的办法就是讲汇合中的元素存在计算机中,遇到一个新元素时,将它和汇合中的元素间接比拟即可。一般来讲,计算机中的汇合是用哈希表(Hash Table)来存储的。它的益处是疾速精确,毛病是消耗存储空间。 为什么说消耗存储空间呢?其根本原因是哈希表办法须要把实实在在的具备特定长度(每个Email地址对应成一个8字节的信息指纹)的元素的信息指纹存储在内存或硬盘中的哈希表中,这个存储量在理论利用中个别是相当大的。比方每存储一亿个Email地址,须要0.8G大小的数字指纹存储空间,思考到哈希表的存储空间利用率个别只有一半,所以须要1.6G的存储空间。如果存储几十亿上百亿的Email地址,那就须要百亿字节的内存存储空间。 而布隆过滤器只须要哈希表1/8到1/4的大小就能解决同样的问题,它实际上是一个很长的二进制向量和一系列的随机映射函数。 上面以WEB页面地址的存储为例来阐明布隆过滤器的工作原理。 假设存储一亿个WEB页面地址,先建设一个16亿二进制(比特),即2亿字节的向量,而后将这16亿个二进制位清零。对于每一个WEB页面地址X,用8个随机数产生器(f1,f2,...,f8)。再用一个随机数产生器G把这8个信息指纹映射到1-16亿中的8个自然数g1,g2,...g8。当初把这8个地位的二进制位都置为1。对着一亿个WEB页面地址都进行这样的解决后,一个针对WEB页面的布隆过滤器就建成了,见下图。 布隆迪过滤器的映射办法 当初,让咱们看看如何用布隆过滤器来检测一个WEB网页地址Y是否曾经被咱们收录。用雷同的8个随机数生成器(f1,f2,...,f8)对这个WEB网页地址产生8个信息指纹s1,s2,...s8,而后将这8个指纹对应到布隆过滤器的8个二进制位,别离是t1,t2,...,t8。如果Y已被收录,显然t1,t2,...,t8对应的8个二进制位肯定是1。通过这样的形式咱们可能很快地确定一个WEB页面是否已被咱们收录。 二、布隆过滤器的实现布隆过滤器实现代码: #encoding=UTF-8 '''Created on 2014年6月21日@author: jin'''import BitVector class MyHash():#哈希类,依据不同参数初始化后作为不同的哈希函数 def __init__(self, cap, seed): self.cap = cap self.seed = seed def hash(self, value): #计算哈希值得过程 ret = 0 for i in range(len(value)): ret += self.seed*ret + ord(value[i]) #ord()函数计算传入的url字符串中每一个字符在ASCII码表中对应的程序值 return (self.cap-1) & ret #返回哈希值,即在比特序列中的地位 class BloomFilter(): def __init__(self, BIT_SIZE=1<<31): self.BIT_SIZE = 1 << 31 #不拢过滤器的比特数, self.seeds = [5, 7, 11, 13,19, 31, 37, 61] #8个种子,用于产生hash函数 self.bitset = BitVector.BitVector(size=self.BIT_SIZE) self.hashFuncList = [] for i in range(len(self.seeds)): self.hashFuncList.append(MyHash(self.BIT_SIZE, self.seeds[i])) #对每个种子,创立一个MyHash对象,一共8个 def insert(self, value): #插入值,这里并非真正地插入并存储,而是把该值对应的8个地位的比特地位为1 for function in self.hashFuncList: locationBit = function.hash(value) #计算应该置为1的比特位 self.bitset[locationBit] = 1 def isContaions(self, value): if value == None: return False ret = True for f in self.hashFuncList: locationBit = f.hash(value) ret = ret & self.bitset[locationBit] #能够看出,对8个哈希函数,只有有一个为0,那么将返回0,即该值尚未存在 return ret def Main(): #主函数 fd = open("urls.txt") #有反复的网址 http://www.kalsey.com/tools/buttonmaker/ bloomfilter = BloomFilter() while True: url = fd.readline() if cmp(url, 'exit') == 0: print 'complete and exit now' break elif bloomfilter.isContaions(url) == False: bloomfilter.insert(url) else: print 'url :%s has exist' % url Main() url.txt外部存储有一系列网址,最初一行是‘exit’,内容如下: ...

November 4, 2020 · 2 min · jiezi

关于数据结构:5-数据结构PHP实现-集合-用链表来实现

1. 特色汇合内的元素不会反复,所以在增加的时候就须要判断是否有元素存在2. 工夫复杂度剖析操作工夫复杂度增加O(1)删除O(n)查问O(n)3. 代码元素结点/** * content: 汇合的元素结点 * create: 2020-11-03 */<?phpnamespace SetBundle;class Node{ /** * 尾指针 * @var Node|null */ protected $tail; /** * 该节点存储额值 * @var mixed */ protected $value; public function __construct($value = null, ?Node $tail = null) { $this->setTail($tail)->setValue($value); } /** * 设置尾结点 * @param Node|null $tail * @return $this */ public function setTail(?Node $tail): self { $this->tail = $tail; return $this; } /** * 获取尾结点 * @return Node|null */ public function getTail(): ?Node { return $this->tail; } /** * 设置值 * @param mixed $value * @return $this */ public function setValue($value): self { $this->value = $value; return $this; } /** * 获取结点里的值 * @return mixed */ public function getValue() { return $this->value; }}汇合的代码<?php/** * content: 汇合(链表实现) * auth: 俞佳明 * create: 2020-11-03 */namespace SetBundle;class LinkSet{ /** * @var Node|null */ protected $node; /** * 汇合大小 * @var int */ protected $size; public function __construct() { $this->node = null; $this->size = 0; } /** * 获取汇合中的元素 * @return int */ public function getSize(): int { return $this->size; } /** * 查问是否存在该值的结点 * @param $value * @return Node|null */ public function contains($value): ?Node { $node = $this->node; while (!is_null($node)) { if ($node->getValue() === $value) { return $node; } $node = $node->getTail(); } return null; } /** * 插入 * @param string|int $value * @return Node * @throws \Exception */ public function add($value): Node { $newNode = new Node($value, null); if (is_null($this->node)) { $this->node = $newNode; } else { if (!is_null($this->contains($value))) { throw new \Exception('插入的元素已存在!'); } $newNode->setTail($this->node); $this->node = $newNode; } $this->size++; return $newNode; } /** * 删除 * @param $value */ public function remove($value) { $node = $this->node; while (!is_null($node) && !is_null($node->getTail())) { if ($node->getTail()->getValue() === $value) { $node->setTail($node->getTail()->getTail()); $this->size--; return; } $node = $node->getTail(); } return; } public function varDump() { $node = $this->node; while (!is_null($node)) { echo $node->getValue(). PHP_EOL; $node = $node->getTail(); } }}4. 示例<?phprequire_once __DIR__ . '/../../vendor/autoload.php';$set = new SetBundleLinkSet();$set->add(1);$set->add(2);$set->add(3);$set->add(4);$set->add(5);$set->add(6);$set->varDump();654321

November 3, 2020 · 2 min · jiezi

关于数据结构:数据结构

什么是数据机构? 官网定义 ———— 数据机构是指相互之间存在着一种或者多种关系的汇合以及该汇合中数据元素之间的关系组成。看了之后脑袋大不?其实简略来讲就是一句话 ———— 数据和数据之间的关系。 一、数据的存储构造 数据的存储构造是指数据的逻辑构造在计算机中的示意。数据的存储构造分为顺序存储构造和链接存储构造两种。 1、顺序存储构造 顺序存储办法它是把逻辑上相邻的结点存储在物理地位相邻的存储单元里,结点间的逻辑关系由存储单元的邻接关系来体现,由 此失去的存储示意称为顺序存储构造,的如下图: 2、链式存储构造 链接存储办法它不要求逻辑上相邻的结点在物理地位上亦相邻,结点间的逻辑关系是由附加的指针字段示意的。由此失去的存储示意 称为链式存储构造,链式存储构造通常借助于程序设计语言中的指针类型来实现. ![image.png](/img/bVcIkKM) 二、数据的逻辑构造 零碎的逻辑构造是从思维的角度上对系du统分类,把零碎分成若干个逻辑单元,不同逻辑单元别离实现本人的性能。数据的逻辑构造是对数据之间关系的形容,有时就把逻辑构造简称为数据结构1、汇合构造:汇合构造的汇合中任何两个数据元素之间都没有逻辑关系,组织模式涣散。2、线性构造:数据结构中线性构造指的是数据元素之间存在着“一对一”的线性关系的数据结构。3、树状构造:树状构造是一个或多个节点的无限汇合。4、图形构造:网络结构是指通信零碎的整体设计,它为网络硬件、软件、协定、存取控制和拓扑提供规范

November 2, 2020 · 1 min · jiezi

关于数据结构:42-数据结构PHP实现-二叉树-二分搜索树的遍历方式递归实现

1. 遍历准则前序遍历:先遍历以后结点,再遍历以后结点的左儿子,最初遍历以后结点的右儿子中序遍历:先遍历以后结点的左儿子,再遍历以后结点,最初遍历以后结点的右儿子后续遍历:先遍历以后结点的左儿子,再遍历以后结点的右儿子,最初遍历以后结点2. 前序遍历示意图 3. 中序遍历示意图 4. 后序遍历示意图 5. 二分搜寻树的递归遍历实现<?phpnamespace TreeBundle;use ArrayBundle\BaseArray;use StackBundle\BaseStack;class BaseBinaryTree{ public function __construct() { $this->rootNode = null; $this->size = 0; } /** * 二叉树的根节点 * @var Node */ protected $rootNode; /** * 获取根节点 * @return Node */ public function getRootNode(): Node { return $this->rootNode; } /** * 二叉树的个数 * @var int */ protected $size; /** * 获取二叉树的元素个数 * @return int */ public function getSize(): int { return $this->size; } /** * 判断是否为空 * @return bool */ public function isEmpty(): bool { return $this->size == 0; } /** * 插入结点 * @param mixed $value * @return void */ public function add($value): void { // 如果不存在根节点就插入根节点 if (is_null($this->rootNode)) { $this->rootNode = new Node($value); $this->size; return; } else { $this->addChild($value, $this->rootNode); return; } } /** * 插入儿子结点 * @param mixed $value * @param Node|null $parentNode */ private function addChild($value, ?Node $parentNode): void { if (bccomp($parentNode->getValue(), $value) > 0) { // 左儿子 if (is_null($parentNode->getLeftChild())) { $parentNode->setLeftChild(new Node($value)); $this->size++; return; } else { $this->addChild($value, $parentNode->getLeftChild()); } } elseif (bccomp($parentNode->getValue(), $value) < 0) { // 右儿子 if (is_null($parentNode->getRightChild())) { $parentNode->setRightChild(new Node($value)); $this->size++; return; } else { $this->addChild($value, $parentNode->getRightChild()); } } else { return; } } const TRAVERSAL_PREORDER = 1; // 先序遍历 const TRAVERSAL_INORDER = 2; // 中序遍历 const TRAVERSAL_POSTORDER = 3; // 后序遍历 /** * 遍历二叉树 * @param int $traversalType * @return string */ public function traversal(int $traversalType = 1): string { $result = ''; switch ($traversalType) { case self::TRAVERSAL_PREORDER: $this->preorderTraversal($result, $this->rootNode); break; case self::TRAVERSAL_INORDER: $this->inorderTraversal($result, $this->rootNode); break; case self::TRAVERSAL_POSTORDER: $this->postorderTraversal($result, $this->rootNode); break; default: break; } return $result; } /** * 先序遍历 * @param string $result 后果值 * @param Node|null $node 结点 * @return void */ protected function preorderTraversal(string &$result, ?Node $node): void { if (is_null($node)) return; // 拼接 if ($result != '') $result .= ' -> '; // 先遍历以后节点 $result .= $node->getValue(); // 再遍历左儿子 $this->preorderTraversal($result, $node->getLeftChild()); // 在遍历右儿子 $this->preorderTraversal($result, $node->getRightChild()); return; } /** * 中序遍历 * @param string $result 后果值 * @param Node|null $node 结点 * @return void */ public function inorderTraversal(string &$result, ?Node $node): void { if (is_null($node)) return; // 先遍历左儿子 $this->inorderTraversal($result, $node->getLeftChild()); // 拼接 if ($result != '') $result .= ' -> '; // 再获取以后结点 $result .= $node->getValue(); // 在遍历右儿子 $this->inorderTraversal($result, $node->getRightChild()); return; } /** * 后序遍历 * @param string $result 后果值 * @param Node|null $node 结点 * @return void */ public function postorderTraversal(string &$result, ?Node $node): void { if (is_null($node)) return; // 先遍历左儿子 $this->postorderTraversal($result, $node->getLeftChild()); // 在遍历右儿子 $this->postorderTraversal($result, $node->getRightChild()); // 拼接 if ($result != '') $result .= ' -> '; // 再获取以后结点 $result .= $node->getValue(); return; }}6. demo<?php// composer主动加载require_once __DIR__ . '/../../vendor/autoload.php';// 获取一个二分搜寻树$tree = new TreeBundleBaseBinaryTree();// 插入结点$tree->add(10);$tree->add(5);$tree->add(15);$tree->add(1);$tree->add(0);$tree->add(2);$tree->add(9);$tree->add(8);$tree->add(11);$tree->add(12);$tree->add(19);$tree->add(18);$tree->add(29);$tree->add(16);$tree->add(17);// 前序遍历echo $tree->traversal(TreeBundleBaseBinaryTree::TRAVERSAL_PREORDER). PHP_EOL;// 中序遍历echo $tree->traversal(TreeBundleBaseBinaryTree::TRAVERSAL_INORDER). PHP_EOL;// 后续遍历echo $tree->traversal(TreeBundleBaseBinaryTree::TRAVERSAL_POSTORDER). PHP_EOL;打印后果: ...

October 30, 2020 · 3 min · jiezi

关于数据结构:41-数据结构PHP实现-二叉树-二分搜索树的结点插入

1. 插入准则:第一个结点为根节点后续插入的结点值如果小于根节点,就成为根节点的左儿子后续插入的结点值如果大于根节点,就成为根节点的右儿子2. 示意图 3. 二分搜寻树的实现<?php/** * content: 二叉树的二分搜寻树的实现 * auth: yujiaming * create: 2020-10-25 */namespace TreeBundle;use ArrayBundle\BaseArray;use StackBundle\BaseStack;class BaseBinaryTree{ public function __construct() { $this->rootNode = null; $this->size = 0; } /** * 二叉树的根节点 * @var Node */ protected $rootNode; /** * 获取根节点 * @return Node */ public function getRootNode(): Node { return $this->rootNode; } /** * 二叉树的个数 * @var int */ protected $size; /** * 获取二叉树的元素个数 * @return int */ public function getSize(): int { return $this->size; } /** * 判断是否为空 * @return bool */ public function isEmpty(): bool { return $this->size == 0; } /** * 插入结点 * @param mixed $value * @return void */ public function add($value): void { // 如果不存在根节点就插入根节点 if (is_null($this->rootNode)) { $this->rootNode = new Node($value); $this->size; return; } else { $this->addChild($value, $this->rootNode); return; } } /** * 插入儿子结点 * @param mixed $value * @param Node|null $parentNode */ private function addChild($value, ?Node $parentNode): void { if (bccomp($parentNode->getValue(), $value) > 0) { // 左儿子 if (is_null($parentNode->getLeftChild())) { $parentNode->setLeftChild(new Node($value)); $this->size++; return; } else { $this->addChild($value, $parentNode->getLeftChild()); } } elseif (bccomp($parentNode->getValue(), $value) < 0) { // 右儿子 if (is_null($parentNode->getRightChild())) { $parentNode->setRightChild(new Node($value)); $this->size++; return; } else { $this->addChild($value, $parentNode->getRightChild()); } } else { return; } }}4. demo<?php// composer主动加载require_once __DIR__ . '/../../vendor/autoload.php';// 获取一个二分搜寻树$tree = new TreeBundleBaseBinaryTree();// 插入根节点$tree->add(10);$tree->add(5);$tree->add(15);$tree->add(1);$tree->add(9);

October 30, 2020 · 1 min · jiezi

关于数据结构:数据结构之存储与逻辑

数据结构相互之间存在一种或多种定义关系的数据元素汇合(collection).逻辑构造是对数据之间关系的形容,它与存储构造,地位无关,同一种逻辑构造能够有多种物理存储构造.能够演绎为两大类: 线性构造是一个数据元素的有序汇合(一对一) 1):汇合之中必存在惟一一个第一元素.2):汇合之中必存在惟一一个最初元素.3):除最初一个元素,都有后继.4):除第一个元素,都有前驱.非线性构造是一个数据元素的关系汇合(一对多,多对多). 1)树2)图物理构造(存储构造)是数据的逻辑构造在物理存储中的映像.它包含数据与关系的示意.数据元素之间的关系在物理存储中有两种不同的示意办法:程序映像与非程序映像.对应的两种不同存储构造为顺序存储构造与链表存储构造.罕用4种存储办法: 顺序存储办法链式存储办法索引存储办法散列存储办法

October 22, 2020 · 1 min · jiezi

关于数据结构:一文详解队列手撸队列的3种方法

本文已收录至我的 Github《算法图解》系列:https://github.com/vipstone/algorithm后面咱们介绍了栈(Stack),队列和栈是比拟像的一种数据结构。咱们能够设想有很多辆汽车正在通过单行道的隧道,所有车辆不能插队、不能掉头,先进来的车也先进来,咱们能够把这种特色的数据结构称之为队列。 队列也属于逻辑构造,所谓的物理构造是指能够将数据存储在物理空间中,比方数组和链表都属于物理数据结构;而逻辑构造则是用于形容数据间的逻辑关系的,它能够由多种不同的物理构造来实现,比方队列和栈都属于逻辑构造。 队列个性队列中的元素必须是先进先出(First In First Out,FIFO)的,它有两个重要的办法:入队(enqueue)和出队(dequeue)。队列的入口端叫队尾(rear),进口端叫队头(front),如下图所示: 手撸队列学习了队列的基本知识之后,接下来咱们将应用代码来实现一个队列。 首先咱们先应用数组来实现一个队列,它的构造如下图所示: 1.自定义队列—数组public class MyQueue<E> { private Object[] queue; // 存储容器 private int head; // 头部指针 private int tail; // 尾部指针 private int size; // 队列理论存储长度 private int maxSize; // 最大容量 public MyQueue() { // 无参构造函数,设置默认参数 this.maxSize = 10; this.head = 0; this.tail = -1; this.size = 0; this.queue = new Object[this.maxSize]; } public MyQueue(int initSize) { // 有参构造函数,设置参数 this.maxSize = initSize; this.head = 0; this.tail = -1; this.size = 0; this.queue = new Object[this.maxSize]; } /** * 查问队头元素 */ public E peek() throws Exception { if (size == 0) { throw new Exception("队列中暂无数据"); } return (E) this.queue[this.head]; } /** * 入列 */ public boolean offer(E e) throws Exception { if (tail >= (maxSize - 1)) { throw new Exception("增加失败,队列已满"); } this.queue[++tail] = e; size++; return true; } /** * 出列 */ public E poll() throws Exception { if (size == 0) { throw new Exception("删除失败,队列为空"); } size--; return (E) this.queue[head++]; } /** * 代码测试 */ public static void main(String[] args) throws Exception { MyQueue queue = new MyQueue(); queue.offer("Hello"); queue.offer("Java"); System.out.println(queue.peek()); queue.poll(); System.out.println(queue.poll()); }}以上代码的执行后果如下: ...

October 21, 2020 · 3 min · jiezi

关于数据结构:最全最详细数据结构与算法视频附课件和源码

源码和课件下载方式在文末什么是数据结构与算法算法用来设计并实现一种用计算机来解决问题的办法。它满足下列性质: 输出:有零个或多个输入量 输入:产生至多一个输出量 确定性:算法的指令清晰、无歧义 有限性:算法的指令执行次数无限,执行工夫无限 咱们在应用计算机解决产问题的过程能够分为上面五个步骤: 问题的了解:搞清楚问题的输出、要求和输入。 数据结构设计:设计能解决问题中数据的数据结构,还要设计能反对算法策略的数据结构。 算法设计:抉择算法策略,用适当的形式形容和逐渐细化算法步骤。 算法剖析:发现有优化的中央,返回第二步,从新设计数据结构和算法  程序实现:用计算机编程,定义数据结构,编写代码实现,并高度和运行。 简介本次课程的亮点在于: 1、由MJ老师与名企算法大咖董甫耸单干独特研发,全程精心实力打造,保障课程的系统性,全面性,高含金量。 2、深刻摸索每一行代码的底层实现,让学习者更好地读懂每一个框架源码,更有效率地应用框架;并且在开发大型项目时,有能力达到性能的极致优化。 3、MJ老师亲自授课,联合大量企业实在案例解说,化繁入简,通俗易懂,同时又留神拓展解说和延长练习。 视频章节目录】 │  01.冒泡、抉择、堆排序.mp4 │  02.插入排序.mp4 │  03.归并排序.mp4 │  04.疾速、希尔排序.mp4 │  05.计数、基数、桶排序.mp4 │  06.并查集.mp4 │  07.图、BFS、DFS、拓扑排序.mp4 │  08.kruskal、prim.mp4 │  09.dijkstra、bellman-ford、floyd.mp4 │  10.KMP、BM、KR、Sunday.mp4 │  11.Dijkstra.mp4 │  12.Dijkstra欠缺、Bellman-Ford.mp4 │  13.递归、回溯.mp4 │  14.尾调用、尾递归、回溯.mp4 │  15.剪枝、N皇后问题(1).mp4 │  16-1.贪婪、分治.mp4 │  16-2.贪婪、分治.mp4 │  17-1.大数乘法、动静布局初步.mp4 │  17-2.大数乘法、动静布局初步.mp4 │  18.最大间断子序列和,最长回升子序列.mp4 │  19-1.最长公共子序列.mp4 │  19-2.最长公共子序列.mp4 │  20-1.最长公共子串、0-1背包问题.mp4 │  20-2.最长公共子串、0-1背包问题.mp4 │  21-1.LIS二分搜寻实现_布隆过滤器.mp4 │  21-2.LIS二分搜寻实现_布隆过滤器.mp4 │  22-1.跳表的搜寻.mp4 │  22-2.跳表的搜寻.mp4 │  23.跳表的增加删除、BPlus树.mp4 4 如何获取视频、课件和源代码视频地址: https://www.bilibili.com/vide...下载源码、课件形式: 公众号首页回复【恋上数据结构】即可获取下载链接 公众号首页回复【恋上数据结构】即可获取下载链接

October 14, 2020 · 1 min · jiezi

关于数据结构:二叉查找树与二叉平衡树

二叉查找树二叉查找树(二叉搜寻树,二叉排序树)若它的左子树不为空,则左子树上所有结点的值均小于它的根结点的值; 若它的右子树不为空,则右子树上所有结点的值均大于它的根结点的值;它的左、右子树也别离为二叉排序树。 二叉均衡树均衡二叉树也称作AVL树,AVL树实质还是一棵二叉查找树,只是在二叉查找树的根底上减少了“均衡”的要求。所谓均衡要求是指,对AVL树的任意结点来说,其左子树与右子树的高度之差的绝对值不超过1,其中左子树与右子树的高度因子之差称为均衡因子。

October 11, 2020 · 1 min · jiezi

关于数据结构:数据结构和算法

第 1 章:稠密数组和队列1、稠密数组1.1、理论需要编写的五子棋程序中,有存盘退出和续上盘的性能因为该二维数组的很多值是默认值 0 ,因而记录了很多没有意义的数据,咱们将其转为稠密数组进行存储 1.2、稠密数组利用1.2.1、稠密数组解决办法稠密数组把具备不同值的元素的行列及值记录在一个小规模的数组中,从而放大程序的规模稠密数组也是二维数组,行数由原数组的数据决定,列数个别为 3 列稠密数组的第一行记录原数组一共有几行几列,有多少个不为零的值 第一列:原数组的行数第二列:原数组的列数第三列:原数组有多少个不为零的值之后的行记录原数组中不为零(x)的值所在的行数、列数以及 x 的值 第一列:x 在原数组中的行数第二列:x 在原数组中的列数第三列:x 的值1.2.2、举例说明原始二维数组较大,压缩后占用空间缩小1.3、利用实例1.3.1、思路剖析应用稠密数组, 来保留相似后面的二维数组(棋盘、 地图等等)把稠密数组存盘, 并且能够从新复原原来的二维数组数 1.3.2、代码实现代码public class SparseArray { public static void main(String[] args) { // 创立一个原始的二维数组 11 * 11 // 0: 示意没有棋子, 1 示意 黑子 2 表蓝子 int chessArr1[][] = new int[11][11]; chessArr1[1][2] = 1; chessArr1[2][3] = 2; chessArr1[4][5] = 2; // 输入原始的二维数组 System.out.println("原始的二维数组~~"); for (int[] row : chessArr1) { for (int data : row) { System.out.printf("%d\t", data); } System.out.println(); } // 将二维数组 转 稠密数组的思 // 1. 先遍历二维数组 失去非0数据的个数 int sum = 0; for (int i = 0; i < chessArr1.length; i++) { for (int j = 0; j < chessArr1[i].length; j++) { if (chessArr1[i][j] != 0) { sum++; } } } // 2. 创立对应的稠密数组 int sparseArr[][] = new int[sum + 1][3]; // 给稠密数组赋值 sparseArr[0][0] = chessArr1.length; sparseArr[0][1] = chessArr1[0].length; sparseArr[0][2] = sum; // 遍历二维数组,将非0的值寄存到 sparseArr中 int count = 0; // count 用于记录是第几个非0数据 for (int i = 0; i < chessArr1.length; i++) { for (int j = 0; j < chessArr1[i].length; j++) { if (chessArr1[i][j] != 0) { count++; sparseArr[count][0] = i; sparseArr[count][1] = j; sparseArr[count][2] = chessArr1[i][j]; } } } // 输入稠密数组的模式 System.out.println(); System.out.println("失去稠密数组为~~~~"); for (int i = 0; i < sparseArr.length; i++) { System.out.printf("%d\t%d\t%d\n", sparseArr[i][0], sparseArr[i][1], sparseArr[i][2]); } System.out.println(); // 将稠密数组 --》 复原成 原始的二维数组 /* * 1. 先读取稠密数组的第一行,依据第一行的数据,创立原始的二维数组,比方下面的 chessArr2 = int [11][11] 2. * 在读取稠密数组后几行的数据,并赋给 原始的二维数组 即可. */ // 1. 先读取稠密数组的第一行,依据第一行的数据,创立原始的二维数组 int chessArr2[][] = new int[sparseArr[0][0]][sparseArr[0][1]]; // 2. 在读取稠密数组后几行的数据(从第二行开始),并赋给 原始的二维数组 即可 for (int i = 1; i < sparseArr.length; i++) { chessArr2[sparseArr[i][0]][sparseArr[i][1]] = sparseArr[i][2]; } // 输入复原后的二维数组 System.out.println(); System.out.println("复原后的二维数组"); for (int[] row : chessArr2) { for (int data : row) { System.out.printf("%d\t", data); } System.out.println(); } }}程序运行后果原始的二维数组~~0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 2 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 2 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 失去稠密数组为~~~~11 11 31 2 12 3 24 5 2复原后的二维数组0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 2 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 2 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0

October 8, 2020 · 3 min · jiezi

关于数据结构:线性表数组和链表

概要线性表是一种线性构造,它是具备雷同类型的n(n≥0)个数据元素组成的无限序列。本文先介绍线性表的几个根本组成部分:数组、单向链表、双向链表;随后给出双向链表的Java语言的实现。 文章转载自: https://www.cnblogs.com/skywa... 数组数组是一种最罕用的线性数据结构,外面寄存的元素的类型是一样的,并且有序的。 数组的特点是:数据是间断的;随机访问速度快。 对于Java而言,Collection汇合中提供了ArrayList和Vector用于数组这种数据结构的实现。 单向链表单向链表(单链表)是链表的一种,它由节点组成,每个节点都蕴含下一个节点的指针。 单链表的示意图如下: 表头为空,表头的后继节点是"节点10"(数据为10的节点),"节点10"的后继节点是"节点20"(数据为10的节点),...单链表删除节点 删除"节点30"删除之前:"节点20" 的后继节点为"节点30",而"节点30" 的后继节点为"节点40"。删除之后:"节点20" 的后继节点为"节点40"。单链表增加节点在"节点10"与"节点20"之间增加"节点15"增加之前:"节点10" 的后继节点为"节点20"。增加之后:"节点10" 的后继节点为"节点15",而"节点15" 的后继节点为"节点20"。 单链表的特点是:节点的链接方向是单向的;绝对于数组来说,单链表的的随机访问速度较慢,然而单链表删除/增加数据的效率很高。 双向链表双向链表(双链表)是链表的一种。和单链表一样,双链表也是由节点组成,它的每个数据结点中都有两个指针,别离指向间接后继和间接前驱。所以,从双向链表中的任意一个结点开始,都能够很不便地拜访它的前驱结点和后继结点。个别咱们都结构双向循环链表。 双链表的示意图如下: 表头为空,表头的后继节点为"节点10"(数据为10的节点);"节点10"的后继节点是"节点20"(数据为10的节点),"节点20"的前继节点是"节点10";"节点20"的后继节点是"节点30","节点30"的前继节点是"节点20";...;开端节点的后继节点是表头。双链表删除节点删除"节点30"删除之前:"节点20"的后继节点为"节点30","节点30" 的前继节点为"节点20"。"节点30"的后继节点为"节点40","节点40" 的前继节点为"节点30"。删除之后:"节点20"的后继节点为"节点40","节点40" 的前继节点为"节点20"。 双链表增加节点在"节点10"与"节点20"之间增加"节点15"增加之前:"节点10"的后继节点为"节点20","节点20" 的前继节点为"节点10"。增加之后:"节点10"的后继节点为"节点15","节点15" 的前继节点为"节点10"。"节点15"的后继节点为"节点20","节点20" 的前继节点为"节点15"。

October 2, 2020 · 1 min · jiezi

关于数据结构:栈与队列简介

栈与队列和数组、链表、树这几种数据结构不太一样。栈与队列次要是做为程序员的工具来应用,它们次要做为构思算法的辅助工具,而不是齐全的数据存储工具。 它们的生命周期比数组那些要短得多,在程序执行期间它们才会被创立,工作执行完就会被销毁。 一 栈栈是一种只能在一端进行插入和删除数据的数据结构,这一端被称为栈顶(top)。其特点简略来讲就是先进后出。栈的次要机制能够用数组来实现,当然也能够用链表来实现。 用数组实现栈,并实现罕用操作——出栈、入栈、查看元素(只能查看栈顶元素)、判断栈是否为空等操作。 public class StackTest { private long[] arr; // 栈顶 private int top; public StackTest(){ arr = new long[10]; top = -1; } public StackTest(int maxsize){ arr = new long[maxsize]; top = -1; } /** * 增加数据 * @param value */ public void push(int value){ arr[++top] = value; } /** * 移除数据 * @return */ public long pop() { return arr[top--]; } /** * 查看数据 * @return */ public long peek(){ return arr[top]; } public boolean isEmpty(){ return top == -1; } /*** * 判断是否满了 * @return */ public boolean isFull(){ return top == arr.length-1; }}栈的所有操作复杂度都为O(1),栈的操作不依赖栈中元素大小,栈不须要挪动和比拟操作。 ...

September 28, 2020 · 2 min · jiezi

关于数据结构:图说各个树的优缺点

前言数据结构的实现有两种形式,一种是顺序存储,一种是链式存储顺序存储:应用一片间断的逻辑地址存放数据链式存储:不必间断地址,应用指针指向下个元素的地址 罕用数据结构有:线性表,栈,队列,树,图等等,二叉树是属于这外面的树。BST(Binary Search Tree)介绍BST 是一种树,他的个性是1.左子树上所有结点的值均小于或等于它的根结点的值。2.右子树上所有结点的值均大于或等于它的根结点的值。3.左、右子树也别离为BST BST的长处:BST 通常是应用链式构造存储实现的,应用顺序存储要么是(Full Binary Tree)或者靠近(Full Binary Tree)顺序存储最大的弊病是必须事后给出数组的存储空间大小插入删除比拟麻烦,长处是能够节俭空间,查找很快BST的毛病:抛开顺序存储和链式存储,BST数据结构自身存在的毛病是,在特定的时候,树的度会始终升高,导致查找效率变低 这个时候咱们就有新的树来解决他的毛病就是(AVL Tree) AVL TreeAVL树,实质上是带了均衡性能的BST,他多了个均衡因子概念即(每个结点的左右子树的高度之差的绝对值(均衡因子)最多为1),如果不满足就会旋转来满足这个条件。下面图一BST 在avl中旋转操作后失去如下,具体怎么旋转的能够在这个网站本人模仿https://www.cs.usfca.edu/~gal...那么AVL 有没有什么毛病呢?最大的毛病就是谋求完满的均衡导致插入和删除须要大量的均衡计算,这个在插入和删除大的状况导致开销较大,这个时候咱们就想着,有没有一种树,解决BST的毛病,同时又不要大量的计算均衡,于是RB-Tree就被创造了 RB-Tree 和 AVL 区别RB-Tree 的定义不介绍了,咱们记得这个区别就行删除node引起树的不均衡时,最坏状况下,AVL须要保护从被删node到root这条门路上所有node的平衡性,而RB-Tree最多只需3次旋转。 下图是解决图一不均衡RB-Tree的图

September 23, 2020 · 1 min · jiezi

关于数据结构:数据结构与算法系列之数组

什么是数组?忘了在哪本书看见过一句话“了解了概念性的货色,你就学会了70%” 回到主题,什么是数组?数组(Array)是一种线性表数据结构。它用一组间断的内存空间,来存储一组具备雷同类型的数据 概念中有两个要害的中央: 数组是一种线性数据结构数组中存储的是间断的内存空间和雷同类型的数据什么是线性数据结构有数据结构根底的小伙伴都应该晓得,线性构造就是数据排成一条线一样的数据结构,也就意味着它仅有前后两个方向,比方队列、单链表、栈等,也是线性构造 与它绝对的就是非线性表,最典型的就是树和图。他们的特点就是并不是只有前后这种关系 间断的内存空间和雷同类型的数据正因为有了这个个性,使得数组能够进行“随机拜访”。尽管拜访数组中某个元素变得很快,但毛病就是在批改(删除、插入)数组的时候,操作会变得很麻烦,因为要保障数组内存空间的连续性,所以不得不进行繁琐的数据挪动 假如有一个长度为5的int类型的数组var a [5]int,假如给这个数组调配的内存空间首地址是1000,则这个数组调配的内存空间为1000~1019,看下图 操作系统会为每一个内存单眼调配一个地址,通过这个地址来拜访内存中的数据。当操作系统须要随机拜访数组中的某一个元素时,它会通过下边这个寻址公式来计算出某个元素的存储地址 a[i]_address = base_address + i * data_type_sizedata_type_size:示意数组中每个元素的大小。比方int类型,它占用的是4个字节,所以这里data_type_size就是4数组和链表的区别是什么通过上边对数组的介绍,能够看进去,数组适宜查找操作。然而查找的工夫复杂度并不为O(1)。即使是排好序的数组,你用二分查找,工夫复杂度也是O(logn)。所以,数组反对随机拜访,依据下标随机拜访的工夫复杂度为O(1) 数组相干操作插入元素数组头部插入元素 在数组的头部插入元素,为了保障空间的连续性,须要将数组中所有的元素向后移一位,而后将待插入元素放入到首部地位 代码实现 function insertIntoHead($elem, $arr) { echo "原始数组:".PHP_EOL; print_r($arr); $i = 0; $len = count($arr); while ($i < $len) { $arr[$len-$i] = $arr[$len-$i-1]; $i++; } $arr[0] = $elem; echo "头部插入元素之后后果:".PHP_EOL; print_r($arr); }数组两头插入元素 在数组的两头某个地位插入元素,须要将待插入地位当前的元素均向后挪动一位 代码实现 function insertIntoMid($elem, $position, $arr) { echo "原始数组:".PHP_EOL; print_r($arr); $len = count($arr); $i = $position-1; $value = $arr[$i]; $arr[$i] = $elem; $head = $i+1; $offset = $position-1; while ($i < $len) { $arr[$len-$i+$offset] = $arr[$len-$i-1+$offset]; $i++; } $arr[$head] = $value; echo "头部插入元素之后后果:".PHP_EOL; print_r($arr); }数组尾部插入元素 ...

September 14, 2020 · 1 min · jiezi

关于数据结构:javascript数据结构之树二叉搜索树平衡二叉树红黑树

一种非程序数据结构--树。树是一种分层数据的形象模型,一个树结构蕴含一系列存在父子关系的节点,每个节点都有一个父节点(除了顶部第一个节点)以及零个或多个子节点,位于树顶部的节点叫根节点。 二叉树和二叉搜寻树二叉树中的节点最多只能有两个节点,一个左侧的节点,一个右侧节点。二叉搜寻树(BST)是二叉树的一种,然而只容许在左侧贮存比父节点小的数据,在右侧贮存比父节点大的数据。 和链表一样,通过援用来示意节点之间的关系,在双向链表里,每个节点有俩援用,一个指向上一个节点,一个指向下一个节点,对于二叉搜寻树也应用同样形式,不同的中央是一个指向左侧节点,一个指向右侧节点(树中会称节点为键)。 insert(key) 树里插入键search(key) 树里查找一个键inOrderTraveres() 中序遍历所有preOrderTraveres() 先序遍历所有postOrderTraveres() 后序遍历所有min() 获取树最小值max() 获取树最大值remove(key) 删除树里某个键const Compare = { LESS_THAN:-1, BIGGER_THAN:1}function defultCompare(a,b){ if(a===b){ return 0; } return a<b?Compare.LESS_THAN:Compare.BIGGER_THAN;}class Node{ constructor(key){ this.key = key; this.left = null; this.right = null; }}class BinarySearchTree{ constructor(compareFn = defaultCompare){ this.compareFn = compareFn; this.root = null; } insert(key){ if(!this.root){ this.root = new Node(key); }else{ this.insertNode(this.root,key); } } insertNode(node,key){//查找比照键大小插入,最初生成构造如上图 if(this.compareFn(key,node.key)==Compare.LESS_THAN){ if(!node.left){ node.left = new Node(key); }else{ this.insertNode(node.left,key); } } else { if(!node.right){ node.right = new Node(key); }else{ this.insertNode(node.right,key); } } } search(key){ return this.searachNode(this.root,key); } searachNode(node,key){ if(!node){ return false; } if(this.compareFn(key,node.key)==Compare.LESS_THAN){ return this.searachNode(node.left,key); }else if(this.compareFn(key,node.key)==Compare.BIGGER_THAN){ return this.searachNode(node.right,key); }else{ return true;//能够依据本人须要返回 } }}做个笔记,该内容借鉴与学习javascript数据结构与算法 ...

September 13, 2020 · 1 min · jiezi

关于数据结构:结构与算法02队列和栈结构

本文源码:GitHub·点这里 || GitEE·点这里 一、队列构造1、根底概念队列是一种非凡的线性表,非凡之处在于它只容许在表的前端(front)进行删除操作,而在表的后端(rear)进行插入操作,和栈一样,队列是一种操作受限制的线性表。进行插入操作的端称为队尾,进行删除操作的端称为队头。 2、特点形容队列是一个有序列表,能够用数组或是链表来实现,遵循先进先出的准则。即:先进入队列的数据,会先取出;后进入队列的数据,要后取出;即FIFO准则。 入队列示意图: 出队列示意图: 通过上述两张图解,不难发现队列构造的一些特点: 先进入的数据先进来;数据从队尾进入,从队首进来;基于数组形容队列下标变更频繁;出队列算法能够基于容器大小取模;队列构造的外围是对容器内是否空、是否满标记的判断算法,即容器为空不可再取,容器已满无奈再存;该算法构造在仓储畛域的适应十分宽泛。 3、音讯队列音讯队列就是基于数据结构中的“先进先出”策略实现的,将音讯以排队的形式放入队列中,而后出队列被生产: 有时候某类音讯生产须要有顺序控制,即能够对音讯中的公共ID做取模解决,即把某类音讯都置于一个队列中即可。 4、API应用案例LinkedList类实现Queue队列接口,因而能够基于LinkedList模仿队列成果。 import java.util.LinkedList;import java.util.Queue;public class M01_Queue { public static void main(String[] args) { // 入队列 Queue<String> queue = new LinkedList<>(); queue.add("head") ; queue.add("middle") ; queue.add("tail") ; // 当队列出数据之后,size是一直变动的 int queueSize = queue.size() ; int loop = 0 ; // 依据队列大小,一直出队列 while (loop < queueSize) { System.out.println(queue.poll()); System.out.println(queue); loop ++ ; } }}二、栈构造1、根底概念栈(stack)又名堆栈,它是一种运算受限的线性表。限定仅在表尾进行插入和删除操作的线性表。这一端被称为栈顶,绝对地,把另一端称为栈底。向一个栈插入新元素又称作进栈、入栈或压栈(push),它是把新元素放到栈顶元素的下面,使之成为新的栈顶元素;从一个栈删除元素又称作出栈或退栈(pop),它是把栈顶元素删除掉,使其相邻的元素成为新的栈顶元素。 ...

September 9, 2020 · 1 min · jiezi

关于数据结构:这是一篇简单的数据结构图解笔记一

前言笔者正在自学数据结构,深感书中的常识偏差实践和代码,而对逻辑自身有些漠视。 次要是图少,字多,以至于学者学习之后对于逻辑的了解不够,最终沦为大学中常见的后果——背书。(笔者接触过一些同学如何学习计算机,深感背书的危害) 为了帮忙他人也帮忙本人了解,我尝试着为常见的几种构造绘制一些动态图。因为工夫仓促,尚未全副实现。 逻辑构造和存储构造逻辑构造只思考数据元素之间的关系,不思考具体实现。存储构造思考在计算机中如何示意。 线性表 逻辑构造:所有数据按线性构造贮存参考元素后面的元素称为前驱参考元素前面的元素称为后继除第一个外,每个元素只有一个前驱处最初一个外,每个元素只有一个后继存储构造: 顺序存储: 提前申请出所有须要的内存地址逻辑地位相邻的数据元素物理地位也是相邻的插入数据时,插入地位之后的所有数据后移长处: 地址间断,查问速度快毛病: 长度固定,不能动静调整插入元素时须要挪动大量元素 链式构造: 应用不间断的内存地址将数据元素分为数据域和指针域数据域存放数据头指针指向第一个元素指针域指向下一个元素插入数据时,扭转插入地位的指针即可长处: 大小可变,灵便又节约空间插入不便毛病:遍历速度慢 栈逻辑构造: 后进先出仅能在栈顶进行操作存储构造: 程序栈: 和程序表相似,一次申请一段间断的地址容量不固定,当初始容量用完后,再按设定的增量进行裁减须要两个指针base和top,别离指向栈底和栈顶插入数据时,top上移;删除数据时,top下移插入数据的地位是top以后指向的地址 链栈 链栈与链表简直统一 应用不间断的内存地址将数据元素分为数据域和指针域数据域存放数据栈顶指针指向第一个元素指针域指向下一个元素不同之处在于: 插入数据时,栈顶指向新插入的元素,新元素的指针域指向原来的栈顶 队列(未完待续)逻辑构造:先进先出 总结本周最大的播种是,通过一番查找和比照,终于把握了超低老本的GIF制作方法动画制作是一门重要技能,再粗疏的语言形容也不如一张图片更活泼,再活泼的图片也不如一段动画更直观将来我会缓缓学残缺本数据结构,之后的每一个知识点都会做一个动画,敬请期待版权申明本文作者:河北工业大学梦云智开发团队 - 刘宇轩 新人经验不足,有倡议欢送交换,有谬误欢送轻喷

September 5, 2020 · 1 min · jiezi

关于数据结构:揭开链表的真面目

链表是一种常见的数据结构,链表是由一连串的结点组成,这个节点就是链结点,每个链结点都由数据域和指针域两局部组成。 应用链表构造能够克服数组构造须要事后晓得数据大小的毛病,链表构造能够充沛利用计算机内存空间,实现灵便的内存动静治理。然而链表失去了数组随机读取的长处,同时链表因为减少了结点的指针域,空间开销比拟大。 链表比拟好的一种了解是:将链表看成一个火车,每个车厢之间都是相互连接的,只有找到火车头,就能够找到具体的车身。链表也是,咱们只关怀它的头。 一 单向链表1.1 单向链表原理图单向链表的一个链结点蕴含数据域和下一个链结点的指针。头结点也蕴含数据域和指针域,然而个别为了不便查找,头节点不写数据,最初一个结点的指针指向空。 1.2 实现单向链表的存储等操作创立一个链结点的实体类 public class Node { // 数据域 public long data; // 指针域 public Node next; public Node(long value){ this.data = value; }}1.2.1 插入一个节点在头节点后插入一个结点,第一步须要将新插入的结点指向头结点指向的结点,第二部将头结点指向新插入的结点。插入结点只须要扭转一个援用,所以复杂度为O(1)。 public class LinkList { private Node head; /** * 在头节点之后插入一个节点 */ public void insertFirst(long value){ Node node = new Node(value); node.next = head; head = node; }}1.2.2 头结点后删除一个结点在头结点后删除一个结点,就是让头结点指向这个结点的下一个结点。复杂度也是O(1)。 public Node deleteFirst(){ Node tmp = head; head = tmp.next; return tmp;}1.2.3 依据数据域查找结点查找须要比对每个结点的数据,实践上查找一个结点均匀须要N/2次,所以复杂度为O(N)。 ...

September 1, 2020 · 2 min · jiezi

关于数据结构:揭开数组的真面目

数组做为一种根底的数据存储构造,利用非常宽泛。数组是用间断的内存空间来存储固定长度的、雷同数据类型的一种数据结构。数据结构是跟语言无关的,这里,应用java来进行数组的相干操作。数组的索引是从0开始的。 一 数组初始化创立数据有两种形式,一种是先申明一个固定长度的数据,而后再给数组赋值,另一种是间接赋值。 第一种: 数据类型[] 数组名称 = new 数据类型[长度];这里的[]标识这申明了一个数组,这个[]除了能够放在数据类型前面,也能够放在数组名词前面,成果一样。如果我申明一个长度为2的long类型的数组,并赋值: long[] arr = new long[2];arr[0] = 1;arr[1] = 2;第二种: 数据类型[] 数组名称 = {元素1,元素2, ...};这样在数组初始化的时候间接给数组赋值,数组的长度由元素的个数决定。 二 自定义类封装数组实现数据操作public class MyArray { // 自定义数组 private long[] arr; // 无效数据长度 private int element; public MyArray(){ arr = new long[9]; } public MyArray(int maxsize){ arr = new long[maxsize]; } /** * 显示数组元素 */ public void display(){ System.out.print("["); for (int i = 0; i < element; i++) { System.out.print(arr[i]+" "); } System.out.print("]"); }}2.1 增加元素数组是用间断的内存空间来存储数据的,则每次增加的时候会往以后数组的最初一个元素上增加元素,一次就能够加上元素,所以它的复杂度为O(1),如果定义一个长度为9数组,数组中曾经有两个元素,则增加第三个元素如下: ...

August 29, 2020 · 2 min · jiezi

关于数据结构:数据同步一致性保障OPPO自研JinS数据同步框架实践

本文来自OPPO互联网根底技术团队,转载请注名作者。同时欢送关注咱们的公众号:OPPO_tech,与你分享OPPO前沿互联网技术及流动。1. 背景随着业务的疾速倒退,对于很多公司来说,构建于单地区的技术体系架构,会面临诸如上面的多种问题: 基础设施的有限性限度了业务的可扩展性。业务扩充到单个数据中心撑不住,次要机房曾经不能再增加机器,但业务却一直要求扩大机房、城市级别的故障灾祸,影响服务的可持续性。整个机房级别的故障时有发生,每次都引起业务的不可用,对公司的形象与支出都造成重大的影响。如2015年杭州某数据中心光缆被挖断,造成某产品业务几个小时的中断,导致重大的损失跨地区的拜访,影响用户体验等。随着全球化脚步的迫近,试想用户客户端与服务一次交互,一次RTT起码须要10ms,若广州到北京网络提早个别为40ms,当用户客户端须要与服务器产生屡次交互时,对用户体验影响就很大,用户体验十分不敌对。OPPO互联网业务倒退疾速,曾经扩大到寰球,随之带来的是技术上的多机房架构,对数据的寰球多机房同步在一致性、响应工夫、吞吐等方面有更高要求。为了解决以上问题带来的影响,本文将从异地多活底层,数据层面摸索。如何给下层业务提供一个平安、牢靠、稳固的数据环境,让下层业务能够集中精力专一业务开发,缩小对数据多活的关注。 1.1 面临的挑战数据多地写入的抵触解决问题远距离两地传输网络问题同步过程中数据一致性问题数据同步的幂等性问题作为同步核心,对于数据复用问题基于上述挑战,调研比照相干的几个支流开源产品,均有其对应的优劣。如某局部产品针对业务场景变动频繁的业务,数据复用问题无奈解决,对源端数据造成微小的压力。某局部产品针对抵触解决方案无奈达成最终的统一状态等。最终咱们基于开源产品的根底上自研 JinS 数据同步框架,全面解决上述问题。 1.2 多活准则异地多活、同城双活、异地灾备等场景,下层业务有几个根本准则须要遵循: 业务内聚:每个残缺的业务流程,须要在一个机房中实现,不容许跨机房调用。只有保障这样一个前提,能力保证数据没有提早,业务数据的状态扭转足够快。可用性优先:当产生故障切换机房时,优先保证系统可用。需容忍无限时间段内的数据不统一,在预先做修复数据正确:在确保可用的根底下,须要防止数据出错,在产生切换或故障时,如果发现某些数据异样抵触导致无奈笼罩,会阻塞至人工染指,保证数据的正确(后续可间接针对数据锁定跳过操作)1.3 产品性能以后产品已实现性能: 双向同步:反对数据源间的增量实时同步,轻松实现异地多活等利用场景单向同步:反对MySQL的单向同步,MySQL -> Redis 异构同步等,帮忙业务简便实现异地灾备,异构同步等场景数据订阅:用于缓存告诉、业务异步解耦,异构数据源的数据实时同步以及复ETL等多种业务场景一致性校验:可实现MySQL端到端的数据一致性校验以及修复数据迁徙:可实现MySQL的数据迁徙,异构迁徙等场景2. 一致性保障摸索咱们将从三个维度的方向去摸索数据同步一致性。首先通过架构设计,保障系统的稳固、牢靠、可扩大。接着会通过以后业内通用的一致性计划,引出数据同步一致性的根本实现。最初通过具体计划施行的维度,来摸索施行过程中的细节。 2.1 架构设计 上图为数据同步的整体架构图。主流程中分为订阅模块和生产模块。订阅模块通过dump协定从源端MySQL 中拉取数据,生产端通过与订阅模块的通信,获取须要同步的数据,写入到指标端中。 订阅模块订阅模块蕴含parser(dump拉取)、sink(过滤)、store(存储)、registry(注册)、monitor(监控)、protocol(协定) 生产模块生产模块蕴含input(与订阅模块交互)、filter(过滤)、output(输入)、registry(注册)、monitor(监控) Manager治理平台模块,负责实现流程一体化。服务部署,分类,监控,告警等流程治理 Consul注册核心,负责实现订阅、生产节点的服务主动注册,与prometheus实现监控主动发现,并于Manager模块实现kv告警指标存储 Consul template通过consul kv实现告警规定文件生成,并交付至prometheus Prometheus、Granfa、AlertManager开源监控平台、开源监控平台可视化界面、开源告警平台 2.2 架构实际因为数据同步中,数据一致性是重中之重。而随着业务的倒退,软件须要一直的迭代。如何从架构层面均衡好其中的关系,使软件在疾速迭代中还要保持稳定、牢靠,是架构施行过程中须要思考的问题。 在理解数据同步架构实际前,先理解一下微内核架构。 2.2.1 微内核架构微内核架构,又称为插件化架构。例如Eclipse这类IDE软件、UNIX这类操作系统、淘宝APP客户端软件、甚至到Dubbo,采纳的都是微内核架构。 微内核架构的设计外围: 插件治理(如何加载插件以及插件加载机会等)插件连贯(如何连贯到外围零碎,如Spring中的依赖注入,eclipse中的OSGI)插件通信(如何实现数据的交互工作) 微内核架构,理论就是面向性能进行拆分的扩展性架构 外围零碎负责具体业务性能无关的通用性能(如插件加载、插件通信等)插件模块负责实现具体业务逻辑2.2.2 数据同步架构数据同步中,生产模块参考微内核架构,将各个模块插件化。registry、monitor、input、filter、output都实现插件可插拔性能。 以Output举例,参考Java SPI、Dubbo SPI机制,基于 “接口 + 策略模式 + 配置文件” 的模式,实现Output的扩大。 配置文件如下: 通过读取配置文件中name的类型,实现对应插件的初始化。主流程中只须要实现接口调用,即可实现插件的调用。 以后生产模块中,已全面实现插件化。订阅模块依然有一部分未实现插件化革新。 2.3 一致性模型摸索章节2.1,2.2中从架构的维度来保障整体的稳定性和扩展性,均衡了软件开发周期中的扩大与稳固之间的关系。上面从实现一致性的角度来摸索数据同步过程中如何达到数据的一致性。 2.3.1 分布式系统问题随着摩尔定律遇到瓶颈,越来越多状况下要依附分布式架构,能力实现海量数据处理能力和可扩大计算能力。如果分布式集群无奈保障处理结果统一的话,那任何建设于其上的业务零碎都无奈失常工作。一致性问题是分布式畛域最根底也是最重要的问题。 因为咱们面临多节点的分布式架构,不可避免会呈现上图中所形容的问题,而这些问题正是形成一致性问题的次要成因。 实现相对现实的严格一致性代价是很大的,通常分为程序一致性和线性一致性。 程序一致性,抽象的说就是保障一个过程内的程序性,多个节点之间的一致性线性一致性,难度更大,让一个零碎看起来像只有一个数据正本,所有操作都是原子性的,抽象说就是多个过程内也须要保障程序性相似Google Spanner,通过 原子时钟 + GPS 的 trueTime 计划,实现分布式数据库的线性一致性。一致性往往是指分布式系统中多个正本对外出现的数据状态,而共识算法,则是形容某一个状态达成统一后果的过程。因而咱们须要辨别,一致性形容的是后果的状态,共识只是实现某一状态统一的伎俩。分布式数据库在通过共识算法实现状态统一的前提下,还须要对多个状态进行程序辨认排序,这是一个相当简单的过程。 ...

August 25, 2020 · 2 min · jiezi

关于数据结构:数据结构-链表

单链表 双链表

August 18, 2020 · 1 min · jiezi

关于数据结构:面经手册-第5篇看图说话讲解23平衡树红黑树的前身

作者:小傅哥博客:https://bugstack.cn 积淀、分享、成长,让本人和别人都能有所播种!????一、前言讲道理5年开发,没用过数据结构,你只是在做CRUD! 很多时候大部分程序员????????头疼于,查问慢、效率低、一堆的关联SQL,次要起因是在程序设计上没有做出很好的数据结构。当然也还有一部分是因为老业务代码,或者没有用到一些大数据服务等。 数据结构、算法、设计模式,是每一个程序员成长过程中的内功心法修炼,而你的新技能用的再绚、多线程使的再6、加锁玩的再牛????,也只能阐明你这个人身材好,但身材好是不能抗住子弹的。只有身材+心法都好,都能纵横捭阖。 这一章节是联合HashMap的延展,在Jdk1.8中HashMap是应用桶数组+链表和红黑树实现,所以顺着上一章节的外围原理和API性能解说后,原本这一章节想间接进入到红黑树,但如果想把红黑树学明确,就须要理解他的前因后果,也就是它的前身2-3树????。 二、面试题谢飞机,考你几个简略的知识点???? 飞机,看你简历写理解数据结构,能够简略介绍下2-3树吗?这种树节点有什么特点,与你理解其余的树结构比照下?你看这个图,向外面插入和删除节点,要怎么操作?????飞机,回去等音讯吧! 三、什么是2-3树日常的学习和一部分搭档的面试中,居然会听????到的是;从HashMap中文红黑树、从数据库索引为B+Tree,但问2-3树的状况就不是很多了。 1. 为什么应用树结构从最基本的起因来看,应用树结构就是为了晋升整体的效率;插入、删除、查找(索引),尤其是索引操作。因为相比于链表,一个均衡树的索引工夫复杂度是O(logn),而数组的索引工夫复杂度是O(n)。 从以下的图上能够比照,两者的索引耗时状况; 从上图能够看到,应用树结构无效的升高工夫复杂度,晋升数据索引效率。另外这个规范的树结构,是二叉搜寻树(Binary Search Tree)。除此之外树形构造还有;AVL树、红黑树、2-3树等2. 二叉搜寻树进化链表在树的数据结构中,最先有点是二叉查找树,也就是英文缩写BST树。在应用数据插入的过程中,现实状况下它是一个均衡的二叉树,但实际上可能会呈现二叉树都一边倒,让二叉树像列表一样的数据结构。从而树形构造的工夫复杂度也从O(logn)降级到O(n),如下图; 二叉搜寻树的数据插入过程是,插入节点与以后树节点做比对,小于在左,大于在右。随着数据的插入程序不同,就会呈现齐全不同的数据结构。可能是一棵均衡二叉树,也极有可能进化成链表的树。当树结构进化成链表当前,整个树索引的性能也跟着进化成链表。综上呢,如果咱们心愿在插入数据后又放弃树的特点,O(logn)的索引性能,那么就须要在插入时进行节点的调整 3. 2-3树解决均衡问题2-3树是什么构造,它怎么解决均衡问题的。带着问题咱们持续????。 2-3树是一种十分奇妙的构造,在放弃树结构的根底上,它容许在一个节点中能够有两个元素,等元素数量等于3个时候再进行调整。通过这种形式呢,来保障整个二叉搜寻树的平衡性。 这样说可能还没有感觉,来看下图; 左侧是二叉搜寻树,右侧是2-3均衡树,别离插入节点4、5,察看树形构造变动。二叉搜寻树开始呈现偏移,节点一遍倒。2-3树通过一个节点中寄存2到3个元素,来调整树形构造,保持平衡。所谓的保持平衡就是从根节点,到每一个最底部的本人点,链路长度统一。2-3树曾经能够解决均衡问题那么,数据是怎么寄存和调整的呢,接下来咱们开始剖析。 四、2-3树应用1. 树结构定义和特点性质2-3树,读法;二三树,个性如下; 序号形容示意图12-,1个数据节点2个树杈23-,2个数据节点3个树杈3三叉与两叉的不同点在于,除了两边的节点,中间件还有一个节点。这个节点是介于2、4之间的值。4当随着插入数据,会呈现长期的一个节点中,有三个元素。这时会被调整成一个二叉树。综上咱们能够总结出,2-3树的一些性质; 2-3树所有子叶节点都在同一层1个节点能够有1到2个数据,如果有三个须要调整树结构1个节点1个数据时,则有两个子节点1个节点2个数据时,则有三个子节点,且两头子节点是介于两个节点间的值2. 数据插入接下来咱们就模仿在二叉搜寻树中进化成链表的数据,插入到2-3树的变动过程,数据包含;1、2、3、4、5、6、7,插入过程图如下; 以上,就是整个数据在插入过程中,2-3树的演化过程,接下来咱们具体解说每一步的变动; ,向节点1插入数据2,此时为了保持平衡,不会新产生分支,只会在一个节点中寄存两个节点。,持续插入数据3,此时这个节点有三数据,1、2、3,是一个长期区域。,把三个数据的节点,两头节点拉起来,调整成树形构造。,持续插入数据4,为了放弃树均衡,会插在节点3的右侧。,持续插入数据5,插入后3、4、5共用1个节点,当一个节点上有三个数据时候,则须要进行调整。,两头节点4向上⏫调整,调整后,1节点在左、3节点在两头、5节点在右。 ,持续插入数据6,在放弃树均衡的状况下,与节点5专用。 ,持续插入数据7,插入后,节点7会与以后的节点 5 6 共用。此时是一个长期寄存,须要调整。初步调整后,抽出6节点,向上寄存,变为2 4 6共用一个节点,这是一个长期状态,还须要持续调整。,因为根节点有三个数据2、4、6,则持续须要把两头节点上移,1、3和5、7 则别离成二叉落到节点2、节点6上。????????希腊字母:(阿尔法)、 (贝塔)、(伽马)、(德尔塔)、(伊普西隆)、(截塔)、(艾塔)、(西塔)、(约塔) 3. 数据删除有了下面数据插入的学习,在看数据删除其实就是一个逆向的过程,在删除的次要包含这样两种状况; 删除了3-节点,也就是蕴含两个数据元素的节点,间接删除即可,不会毁坏树均衡。删除了2-节点,此时会毁坏树均衡,须要将树高缩短或者元素合并,复原树均衡。承接下面????的例子,咱们把数据再从7、6、5、4、3、2、1程序删除,察看2-3树的构造变动,如下; ,删除节点7,因为节点7只有一个数据元素,删除节点5、6合并,但此时毁坏了2-3树的平衡性,须要缩短树高进行调整。,因为删除节点后,整个树结构不均衡,所以须要缩短树高,调整元素。节点2、4合并,节点1、3别离插入左侧和两头。,删除节点6,这个节点是3-节点(能够分出3个叉的意思),删除后不会毁坏树均衡,放弃不变。,删除节点5,此时会毁坏树均衡,须要把跟节点4下放,与3合并。,删除节点4,这个节点仍旧是3-节点,所以不须要扭转树结构。,删除节点3,此时只有1、2节点,须要合并。 ,删除节点2,此时节点仍旧是3-节点,所以不须要扭转树结构。再看一个略微简单点2-3树删除: 下面????这张图,就一个略微简单点的2-3均衡树,树的删除过程次要包含; 删除4,其实须要将节点3、5合并,指向节点2,放弃树均衡。删除7,节点8、9合并。删除14,节点15上移,复原成3-叉树。????如果有时候不好了解删除,能够试想下,这个要删除的节点,在插入的时候是一个什么成果。 4. 数据索引相比于插入和删除,索引的过程还是比较简单的,不须要调整数据后果。根本准则就是; 小于以后节点值,左侧寻找大于以后节点值,右侧寻找始终到找到索引值,进行。????第一层寻找: ????第二层寻找: ????第三次寻找: 五、总结综上解说了2-3树????的核心内容,通过本章节的学习,能够理解2-3树是一种怎么的数据结构、如何插入数据、删除数据以及数据的索引,同时要晓得这是一种均衡树的构造,包含2-叉和3-叉节点以及数构造随着数据的增加删除调整。2-3树是红黑树的演变前身,通过这一章节的学习就很容易学习红黑树的相干常识,在红黑树中增加数据进行的渲染、旋转等来放弃树均衡。红黑树靠近均衡数据结构方面的常识学习起来,可能会比拟????烧脑,因为须要思考出那种模型构造和变动的过程,所以会感觉艰难。但这个烧脑的过程也是对学习十分有帮忙的,能够迅速建设常识凸起,当冲破不了解到了解,能够有十分多的播种。六、系列举荐面试官都问我啥HashMap外围常识,扰动函数、负载因子、扩容链表拆分,深度学习HashMap数据插入、查找、删除、遍历,源码剖析重学 Java 设计模式,内功心法修炼,22个互联网实在业务场景实战DDD畛域驱动设计实际,畛域层决策规定树服务设计

August 17, 2020 · 1 min · jiezi

关于数据结构:剑指offer4根据前序中序构造二叉树JavaPython

依据前序,中序结构二叉树1. 题目形容输出某二叉树的前序遍历和中序遍历的后果,请重建出该二叉树。假如输出的前序遍历和中序遍历的后果中都不含反复的数字。 2. 示例例如,给出 前序遍历 preorder = [3,9,20,15,7] 根, 左, 右 中序遍历 inorder = [9,3,15,20,7] 左, 根, 右 返回如下的二叉树: 3. 解题思路思路:前序的第一个元素是根结点的值,在中序中找到该值,中序中该值的右边的元素是根结点的左子树,左边是右子树,而后递归的解决右边和左边 具体来说,前序遍历的第一个值肯定为根节点,对应于中序遍历两头的一个点。在中序遍历序列中,这个点左侧的均为根的左子树,这个点右侧的均为根的右子树。这时能够利用递归,别离取前序遍历[1:i+1]和中序遍历的[:i]对应与左子树持续上一个过程,取前序遍历[i+1:]和中序遍历[i+1]对应于右子树持续上一个过程,最终得以重建二叉树。 Python解决会比较简单一些 4. Java实现/** * Definition for binary tree * public class TreeNode { * int val; * TreeNode left; * TreeNode right; * TreeNode(int x) { val = x; } * } */ import java.util.Arrays; public class Solution { public TreeNode reConstructBinaryTree(int [] pre,int [] tin) { if (pre.length == 0 || tin.length == 0){ return null; } TreeNode root = new TreeNode(pre[0]); // 找到 第一个元素在 tin 中的地位 int i = getIndex(tin, pre[0]); root.left = reConstructBinaryTree(Arrays.copyOfRange(pre, 1, i+1), Arrays.copyOfRange(tin, 0, i)); root.right = reConstructBinaryTree(Arrays.copyOfRange(pre, i+1, pre.length), Arrays.copyOfRange(tin, i+1, tin.length)); return root; } private int getIndex(int[] array, int value){ int index = 0; for (int i = 0; i < array.length; i++){ if (array[i] == value){ index = i; break; } } return index; }}5. Python实现'''输出某二叉树的前序遍历和中序遍历的后果,请重建出该二叉树。假如输出的前序遍历和中序遍历的后果中都不含反复的数字。例如输出前序遍历序列{1,2,4,7,3,5,6,8}和中序遍历序列{4,7,2,1,5,3,8,6},则重建二叉树并返回。''' class TreeNode: def __init__(self, x): self.val = x self.left = None self.right = Noneclass Solution: # 返回结构的TreeNode根节点 def reConstructBinaryTree(self, pre, tin): if not pre or not tin: #只有一个数组为空,则退出函数 return if set(pre) != set(tin):#如果数组中有反复元素 return root = TreeNode(pre[0]) i = tin.index(pre[0]) root.left = self.reConstructBinaryTree(pre[1: i+1], tin[: i]) root.right = self.reConstructBinaryTree(pre[i+1: ], tin[i+1: ]) return root pre = [1, 2, 3, 5, 6, 4]tin = [5, 3, 6, 2, 4, 1]test = Solution()newTree = test.reConstructBinaryTree(pre, tin)print(newTree)#中序遍历def inorder(newTree): if newTree: inorder(newTree.left) print(newTree.val) inorder(newTree.right)inorder(newTree) ...

August 15, 2020 · 2 min · jiezi

关于数据结构:剑指offer4根据前序中序构造二叉树JavaPython

依据前序,中序结构二叉树1. 题目形容输出某二叉树的前序遍历和中序遍历的后果,请重建出该二叉树。假如输出的前序遍历和中序遍历的后果中都不含反复的数字。 2. 示例例如,给出 前序遍历 preorder = [3,9,20,15,7] 根, 左, 右 中序遍历 inorder = [9,3,15,20,7] 左, 根, 右 返回如下的二叉树: 3. 解题思路思路:前序的第一个元素是根结点的值,在中序中找到该值,中序中该值的右边的元素是根结点的左子树,左边是右子树,而后递归的解决右边和左边 具体来说,前序遍历的第一个值肯定为根节点,对应于中序遍历两头的一个点。在中序遍历序列中,这个点左侧的均为根的左子树,这个点右侧的均为根的右子树。这时能够利用递归,别离取前序遍历[1:i+1]和中序遍历的[:i]对应与左子树持续上一个过程,取前序遍历[i+1:]和中序遍历[i+1]对应于右子树持续上一个过程,最终得以重建二叉树。 Python解决会比较简单一些 4. Java实现/** * Definition for binary tree * public class TreeNode { * int val; * TreeNode left; * TreeNode right; * TreeNode(int x) { val = x; } * } */ import java.util.Arrays; public class Solution { public TreeNode reConstructBinaryTree(int [] pre,int [] tin) { if (pre.length == 0 || tin.length == 0){ return null; } TreeNode root = new TreeNode(pre[0]); // 找到 第一个元素在 tin 中的地位 int i = getIndex(tin, pre[0]); root.left = reConstructBinaryTree(Arrays.copyOfRange(pre, 1, i+1), Arrays.copyOfRange(tin, 0, i)); root.right = reConstructBinaryTree(Arrays.copyOfRange(pre, i+1, pre.length), Arrays.copyOfRange(tin, i+1, tin.length)); return root; } private int getIndex(int[] array, int value){ int index = 0; for (int i = 0; i < array.length; i++){ if (array[i] == value){ index = i; break; } } return index; }}5. Python实现'''输出某二叉树的前序遍历和中序遍历的后果,请重建出该二叉树。假如输出的前序遍历和中序遍历的后果中都不含反复的数字。例如输出前序遍历序列{1,2,4,7,3,5,6,8}和中序遍历序列{4,7,2,1,5,3,8,6},则重建二叉树并返回。''' class TreeNode: def __init__(self, x): self.val = x self.left = None self.right = Noneclass Solution: # 返回结构的TreeNode根节点 def reConstructBinaryTree(self, pre, tin): if not pre or not tin: #只有一个数组为空,则退出函数 return if set(pre) != set(tin):#如果数组中有反复元素 return root = TreeNode(pre[0]) i = tin.index(pre[0]) root.left = self.reConstructBinaryTree(pre[1: i+1], tin[: i]) root.right = self.reConstructBinaryTree(pre[i+1: ], tin[i+1: ]) return root pre = [1, 2, 3, 5, 6, 4]tin = [5, 3, 6, 2, 4, 1]test = Solution()newTree = test.reConstructBinaryTree(pre, tin)print(newTree)#中序遍历def inorder(newTree): if newTree: inorder(newTree.left) print(newTree.val) inorder(newTree.right)inorder(newTree) ...

August 15, 2020 · 2 min · jiezi

关于数据结构:剑指offer3-从尾到头打印单链表值JavaPython

从尾到头打印单链表值1. 题目形容输出一个链表,按链表从尾到头的程序返回一个ArrayList。 2. 示例无 3. 解题思路此题比较简单 第一种办法:应用数组。先从头到尾读取链表数据,保留到一个数组a中。因为要获取从尾到头数据,新开一个数组b,从数组a尾部到头部开始读取,保留到数组b中。 第二种办法:应用栈。先从头到尾读取链表数据,保留到一个栈a中。利用栈先进后出的性质,pop到数组a中。 4. Java实现办法一:应用数组/*** public class ListNode {* int val;* ListNode next = null;** ListNode(int val) {* this.val = val;* }* }**/import java.util.ArrayList;public class Solution { public ArrayList<Integer> printListFromTailToHead(ListNode listNode) { ArrayList<Integer> res = new ArrayList(); if (listNode == null){ return res; } while(listNode != null){ res.add(listNode.val); listNode = listNode.next; } ArrayList<Integer> ress = new ArrayList(); for(int i = res.size()-1; i >= 0; i--){ ress.add(res.get(i)); } return ress; }}办法二:应用栈/*** public class ListNode {* int val;* ListNode next = null;** ListNode(int val) {* this.val = val;* }* }**/import java.util.ArrayList;import java.util.Stack; public class Solution { public ArrayList<Integer> printListFromTailToHead(ListNode listNode) { // 应用栈构造实现 ArrayList<Integer> res = new ArrayList(); if (listNode == null){ return res; } Stack<Integer> stack = new Stack(); while (listNode != null){ stack.push(listNode.val); listNode = listNode.next; } while (!stack.isEmpty()){ res.add(stack.pop()); } return res; }}5. Python实现第一种办法应用栈第二种间接数组头插法,代码简洁,然而复杂度较高 ...

August 15, 2020 · 2 min · jiezi

关于数据结构:剑指offer3-从尾到头打印单链表值JavaPython

从尾到头打印单链表值1. 题目形容输出一个链表,按链表从尾到头的程序返回一个ArrayList。 2. 示例无 3. 解题思路此题比较简单 第一种办法:应用数组。先从头到尾读取链表数据,保留到一个数组a中。因为要获取从尾到头数据,新开一个数组b,从数组a尾部到头部开始读取,保留到数组b中。 第二种办法:应用栈。先从头到尾读取链表数据,保留到一个栈a中。利用栈先进后出的性质,pop到数组a中。 4. Java实现办法一:应用数组/*** public class ListNode {* int val;* ListNode next = null;** ListNode(int val) {* this.val = val;* }* }**/import java.util.ArrayList;public class Solution { public ArrayList<Integer> printListFromTailToHead(ListNode listNode) { ArrayList<Integer> res = new ArrayList(); if (listNode == null){ return res; } while(listNode != null){ res.add(listNode.val); listNode = listNode.next; } ArrayList<Integer> ress = new ArrayList(); for(int i = res.size()-1; i >= 0; i--){ ress.add(res.get(i)); } return ress; }}办法二:应用栈/*** public class ListNode {* int val;* ListNode next = null;** ListNode(int val) {* this.val = val;* }* }**/import java.util.ArrayList;import java.util.Stack; public class Solution { public ArrayList<Integer> printListFromTailToHead(ListNode listNode) { // 应用栈构造实现 ArrayList<Integer> res = new ArrayList(); if (listNode == null){ return res; } Stack<Integer> stack = new Stack(); while (listNode != null){ stack.push(listNode.val); listNode = listNode.next; } while (!stack.isEmpty()){ res.add(stack.pop()); } return res; }}5. Python实现第一种办法应用栈第二种间接数组头插法,代码简洁,然而复杂度较高 ...

August 15, 2020 · 2 min · jiezi

关于数据结构:剑指offer2替换空格JavaPython

替换空格1. 题目形容请实现一个函数,将一个字符串中的空格替换成“%20”。 2. 示例例如,当字符串为We Are Happy.则通过替换之后的字符串为We%20Are%20Happy。 3. 解题思路此题比较简单 第一种办法:新开一个内存空间,遍历原字符串数组,如果碰到字符为空格,则append %20进新的空间 第二种办法:不开拓新的空间,首先统计空格数量,从而计算出新的空间大大小,从前面往前面挪动。 4. Java实现办法一:新开一个内存空间// 新开一个内存空间public class Solution { public String replaceSpace(StringBuffer str) { //创立一个新的空间 StringBuffer out = new StringBuffer(); for (int i = 0; i < str.length(); i++){ char a = str.charAt(i); if (a == ' '){ out.append("%20"); }else{ out.append(a); } } return out.toString(); }}办法二:不开拓新的空间,从前面往前面挪动public class Solution { public String replaceSpace(StringBuffer str) { // 计算空格的数量 int spaceNum = 0; for (int i = 0; i < str.length(); i++){ char a = str.charAt(i); if (a == ' '){ spaceNum ++; } } // 开拓空间 int oldIndex = str.length()-1; // 原字符串的下标 int newLength = str.length() + spaceNum * 2; int newIndex = newLength -1; str.setLength(newLength); // 从新设置字符串的长度 while(newIndex >= 0){ if (str.charAt(oldIndex) != ' '){ str.setCharAt(newIndex, str.charAt(oldIndex)); oldIndex --; newIndex --; }else{ str.setCharAt(newIndex--, '0'); str.setCharAt(newIndex--, '2'); str.setCharAt(newIndex--, '%'); oldIndex--; // 只进行一次减 1 } } return str.toString(); }}5. Python实现办法一:下列的字符串是 不可变对象,应用 + 操作时,会产生许多的对象,所以最好应用第二种办法class Solution(): def replaceSpace(self, s): if type(s) != str: return new_s = '' for sr in s: if sr == ' ': new_s += '%20' else: new_s += sr return new_s so = Solution()s = ' ab c d e 'print(so.replaceSpace(s))# %20ab%20c%20d%20e%20print(s.replace(' ', '%20'))办法二:转换成列表模式class Solution: def replaceSpace(self, s): if type(s) != str: return li = list(s) for i in range(len(li)): if li[i] == ' ': li[i] = '%20' res = "".join(li) return resso = Solution()s = ' ab c d e 'print(so.replaceSpace(s))如果您感觉本文有用,请点个“在看” ...

August 15, 2020 · 2 min · jiezi

关于数据结构:剑指offer2替换空格JavaPython

替换空格1. 题目形容请实现一个函数,将一个字符串中的空格替换成“%20”。 2. 示例例如,当字符串为We Are Happy.则通过替换之后的字符串为We%20Are%20Happy。 3. 解题思路此题比较简单 第一种办法:新开一个内存空间,遍历原字符串数组,如果碰到字符为空格,则append %20进新的空间 第二种办法:不开拓新的空间,首先统计空格数量,从而计算出新的空间大大小,从前面往前面挪动。 4. Java实现办法一:新开一个内存空间// 新开一个内存空间public class Solution { public String replaceSpace(StringBuffer str) { //创立一个新的空间 StringBuffer out = new StringBuffer(); for (int i = 0; i < str.length(); i++){ char a = str.charAt(i); if (a == ' '){ out.append("%20"); }else{ out.append(a); } } return out.toString(); }}办法二:不开拓新的空间,从前面往前面挪动public class Solution { public String replaceSpace(StringBuffer str) { // 计算空格的数量 int spaceNum = 0; for (int i = 0; i < str.length(); i++){ char a = str.charAt(i); if (a == ' '){ spaceNum ++; } } // 开拓空间 int oldIndex = str.length()-1; // 原字符串的下标 int newLength = str.length() + spaceNum * 2; int newIndex = newLength -1; str.setLength(newLength); // 从新设置字符串的长度 while(newIndex >= 0){ if (str.charAt(oldIndex) != ' '){ str.setCharAt(newIndex, str.charAt(oldIndex)); oldIndex --; newIndex --; }else{ str.setCharAt(newIndex--, '0'); str.setCharAt(newIndex--, '2'); str.setCharAt(newIndex--, '%'); oldIndex--; // 只进行一次减 1 } } return str.toString(); }}5. Python实现办法一:下列的字符串是 不可变对象,应用 + 操作时,会产生许多的对象,所以最好应用第二种办法class Solution(): def replaceSpace(self, s): if type(s) != str: return new_s = '' for sr in s: if sr == ' ': new_s += '%20' else: new_s += sr return new_s so = Solution()s = ' ab c d e 'print(so.replaceSpace(s))# %20ab%20c%20d%20e%20print(s.replace(' ', '%20'))办法二:转换成列表模式class Solution: def replaceSpace(self, s): if type(s) != str: return li = list(s) for i in range(len(li)): if li[i] == ' ': li[i] = '%20' res = "".join(li) return resso = Solution()s = ' ab c d e 'print(so.replaceSpace(s))如果您感觉本文有用,请点个“在看” ...

August 15, 2020 · 2 min · jiezi

关于数据结构:数据结构之二分搜索树

二分搜寻树什么是树? 树是一种重要的非线性数据结构,直观地看,它是数据元素(在树中称为结点)按分支关系组织起来的构造,很象自然界中的树那样。二分搜寻树的每一个节点的值都大于左子树的所有节点的值,同时小于其右子树的所有节点的值,每一颗子树都是一颗二分搜寻树。 须要留神的是二分搜寻树并不一定每个节点都有子节点,并且存储的元素必须有可比拟值。 向二分搜寻树增加元素 如上图所示,咱们如果想插入28元素,咱们须要判断插入在根节点的左孩子还是右孩子,28比41小,所以要插入在左孩子里,而后比拟22,发现比22大所以要比拟22的右孩子,而后比拟33,发现比33小所以就比拟33的左孩子,然而发现33没有左孩子了,这个时候咱们就能够将28插入在33的左孩子地位。 应用递归实现增加操作 public class BST<E extends Comparable<E>> { private class Node { private E e; //定义左节点和右节点 private Node left, right; public Node(E e) { this.e = e; left = null; right = null; } } private Node root; private int size; public int size() { return size; } public boolean isEmpty() { return size == 0; } public void add(E e) { root = add(root, e); } //向以node为根的二分搜寻树中插入元素E,递归算法 private Node add(Node node, E e) { if (node == null) { size++; return new Node(e); } if (e.compareTo(node.e) < 0) { node.left = add(node.left, e); } else if (e.compareTo(node.e) > 0) { node.right = add(node.right, e); } return node; }}二分搜寻树查问元素具体实现逻辑和增加相似,都是应用递归来实现。 ...

August 14, 2020 · 3 min · jiezi

关于数据结构:数据结构之栈和队列

栈和队列栈也是一种线性构造相比数组,栈对应的操作是数组的子集只能从一端增加元素,也只能从一端取出元素这一端称为栈顶栈是一种后进先出的数据结构(LIFO) 举一个例子,咱们平时敲代码都常常撤销,而撤销操作的原理就是靠栈来实现的。 比方咱们先打出 "沉迷"、"学习"、"不法",这个时候咱们发现打错了无奈两个字,而这个时候栈是这样的。 通过应用撤销性能,不法将从栈顶取出,而后删除不法两个字。这个时候栈中就只有两个元素。 再举一个深刻的例子:程序调用的零碎栈 假如咱们有三个函数。 咱们通过调用函数A,在执行第二行执行B函数的时候,暂停函数A。这个时候就会将暂停的函数A压入栈中。 而后调用函数B里的C函数时候,也会将函数B压入栈中, 当函数C执行完了当前,会首先取出栈中的B2,而后继续执行B2剩下的函数,执行实现当前执行A2。 栈的根本实现定义一个Stack接口和实现类 public interface Stack<E> { int getSize(); boolean isEmpty(); void push(E e); E pop(); E peek();} public class ArrayStack<E> implements Stack<E>{ private Array<E> array; public ArrayStack(){ array = new Array<>(); } public ArrayStack(int capacity){ array = new Array<>(capacity); } @Override public int getSize() { return array.getSize(); } @Override public boolean isEmpty() { return array.isEmpty(); } //增加到栈顶 @Override public void push(E e) { array.addLast(e); } //取出并移除栈顶元素 @Override public E pop() { return array.removeLast(); } //查看栈顶元素 @Override public E peek() { return array.get(array.getSize() - 1); } public int getCapacity(){ return array.getCapacity(); } @Override public String toString() { StringBuilder res = new StringBuilder(); res.append("Stack: "); res.append("["); for (int i = 0; i < array.getSize(); i++) { res.append(array.get(i)); if (i != array.getSize() - 1) { res.append(", "); } } res.append("] top"); return res.toString(); }}创立测试类 ...

August 14, 2020 · 4 min · jiezi

关于数据结构:数据结构之栈和队列

栈和队列栈也是一种线性构造相比数组,栈对应的操作是数组的子集只能从一端增加元素,也只能从一端取出元素这一端称为栈顶栈是一种后进先出的数据结构(LIFO) 举一个例子,咱们平时敲代码都常常撤销,而撤销操作的原理就是靠栈来实现的。 比方咱们先打出 "沉迷"、"学习"、"不法",这个时候咱们发现打错了无奈两个字,而这个时候栈是这样的。 通过应用撤销性能,不法将从栈顶取出,而后删除不法两个字。这个时候栈中就只有两个元素。 再举一个深刻的例子:程序调用的零碎栈 假如咱们有三个函数。 咱们通过调用函数A,在执行第二行执行B函数的时候,暂停函数A。这个时候就会将暂停的函数A压入栈中。 而后调用函数B里的C函数时候,也会将函数B压入栈中, 当函数C执行完了当前,会首先取出栈中的B2,而后继续执行B2剩下的函数,执行实现当前执行A2。 栈的根本实现定义一个Stack接口和实现类 public interface Stack<E> { int getSize(); boolean isEmpty(); void push(E e); E pop(); E peek();} public class ArrayStack<E> implements Stack<E>{ private Array<E> array; public ArrayStack(){ array = new Array<>(); } public ArrayStack(int capacity){ array = new Array<>(capacity); } @Override public int getSize() { return array.getSize(); } @Override public boolean isEmpty() { return array.isEmpty(); } //增加到栈顶 @Override public void push(E e) { array.addLast(e); } //取出并移除栈顶元素 @Override public E pop() { return array.removeLast(); } //查看栈顶元素 @Override public E peek() { return array.get(array.getSize() - 1); } public int getCapacity(){ return array.getCapacity(); } @Override public String toString() { StringBuilder res = new StringBuilder(); res.append("Stack: "); res.append("["); for (int i = 0; i < array.getSize(); i++) { res.append(array.get(i)); if (i != array.getSize() - 1) { res.append(", "); } } res.append("] top"); return res.toString(); }}创立测试类 ...

August 14, 2020 · 4 min · jiezi

关于数据结构:数据结构之数组

数组什么是数组? 数组就是把数据码成一排进行寄存 须要留神的是数组的索引是从0开始应用Java中的数组public static void main(String[] args) { //定义数组以及长度为10 int[] arr = new int[10]; for (int i = 0; i < arr.length; i++) { arr[i] = i; } //定义数组以及数据 int[] scores = new int[]{100, 99, 98}; //批改第0个值为10 scores[0] = 10; for (int score : scores) { System.out.println(score); }}封装属于咱们的数组数组最大的长处:疾速查问。 scores[2]数组最好利用于"索引有语意"的状况。然而并非所有有语意的索引都实用于数组例如:身份证号,如果想用身份证号来当索引的话,咱们要开拓一个很大的空间。 索引没有语意的状况下,如何示意没有元素? 如何增加元素?如何删除元素? 基于Java的数组,二次封装属于咱们本人的数组类。 public class Array { private int[] data; private int size; //无参数的构造函数,默认的数组容量大小为10 public Array() { this(10); } //构造函数,传入数组的容量capacity结构Array public Array(int capacity) { data = new int[capacity]; size = 0; } //获取数组中的元素个数 public int getSize() { return size; } //获取数组的容量 public int getCapacity() { return data.length; } public boolean isEmpty() { return size == 0; } }向数组中增加元素 ...

August 14, 2020 · 6 min · jiezi

关于数据结构:数据结构之栈

栈是一个先入后出(FILO-FirstInLastOut)的有序列表。 栈是限度线性表中元素的插入和删除只能在线性表的同一端进行的一种非凡线性表。容许插入和删除的一端,为变动的一端,称为栈顶(Top),另一端为固定的一端,称为栈底(Bottom)。依据栈的定义可知,最先放入栈中元素在栈底,最初放入的元素在栈顶,而删除元素刚好相同,最初放入的元素最先删除,最先放入的元素最初删除。 public class ArrayStackDemo { public static void main(String[] args) { ArrayStack stack = new ArrayStack(5); stack.push(1); stack.push(2); stack.push(3); stack.push(4); stack.push(5); stack.list(); stack.pop(); stack.list(); }}//定义一个 ArrayStack 示意栈class ArrayStack { private int maxSize; // 栈的大小 private int[] stack; // 数组,数组模仿栈,数据就放在该数组 private int top = -1;// top示意栈顶,初始化为-1 //结构器 public ArrayStack(int maxSize) { this.maxSize = maxSize; stack = new int[this.maxSize]; } //栈满 public boolean isFull() { return top == maxSize - 1; } //栈空 public boolean isEmpty() { return top == -1; } //入栈-push public void push(int value) { //先判断栈是否满 if(isFull()) { System.out.println("栈满"); return; } top++; stack[top] = value; } //出栈-pop, 将栈顶的数据返回 public int pop() { //先判断栈是否空 if(isEmpty()) { //抛出异样 throw new RuntimeException("栈空,没有数据~"); } int value = stack[top]; top--; return value; } //显示栈的状况[遍历栈], 遍历时,须要从栈顶开始显示数据 public void list() { if(isEmpty()) { System.out.println("栈空,没有数据~~"); return; } //须要从栈顶开始显示数据 for(int i = top; i >= 0 ; i--) { System.out.println(String.valueOf(stack[i])); } } }中断表达式实现计算器思路剖析:筹备两个栈,一个存数,一个存运算符1.通过一个index值遍历表达式2.如果发现以后是一个数字,间接入数栈3.如果发现以后是一个符号 ...

July 20, 2020 · 4 min · jiezi

数据结构之链表

1.链表是以节点的形式来存储,是链式存储2.每个节点蕴含 data 域,next 域:指向下一个节点3.链表的各个节点不肯定是间断存储4.链表分带头节点的链表和没有头节点的链表,依据理论的需要来确定 单链表a.减少节点思路剖析借助辅助节点,找到最初一个节点的next指向新节点b.删除节点思路剖析借助辅助节点,找到删除节点的前一个节点temptemp.next = temp.next.nextc.批改节点思路剖析借助辅助节点,遍历找到该节点而后批改d.遍历链表思路剖析借助辅助节点,遍历e.获取到单链表的节点的个数遍历,累加f.查找单链表中的倒数第k个节点遍历至总长度-k的节点即为倒数第k个节点g.单链表的反转新建反转头节点reverseHead,遍历原链表,将节点一个个插入反转俩表的第一个节点,最初再把head.next = reverseHead.nexth.从头到尾打印单链表用栈先进后出个性 import java.util.Stack;public class SingleLinkedListDemo { public static void main(String[] args) { SingleLinkedList singleLinkedList = new SingleLinkedList(); Node node1 = new Node(1,"1"); Node node2 = new Node(2,"2"); Node node3 = new Node(3,"3"); Node node4 = new Node(4,"4"); singleLinkedList.add(node1); singleLinkedList.add(node2); singleLinkedList.add(node3); singleLinkedList.add(node4); singleLinkedList.list();// singleLinkedList.del("3");// singleLinkedList.list();// singleLinkedList.update(new Node(2,"10"));// singleLinkedList.list();// singleLinkedList.reversetList();// singleLinkedList.list(); singleLinkedList.reversePrint(); }}class SingleLinkedList { //先初始化一个头节点, 头节点不要动 private Node head = new Node(0,""); //增加节点到单向链表 //1. 找到以后链表的最初节点 //2. 将最初这个节点的next指向新的节点 public void add(Node Node) { //因为head节点不能动,因而咱们须要一个辅助遍历 temp Node temp = head; //遍历链表,找到最初 while (temp.next != null) { temp = temp.next; } //当退出while循环时,temp就指向了链表的最初 //将最初这个节点的next 指向新的节点 temp.next = Node; } //批改节点的信息, 依据no编号来批改 public void update(Node newNode) { //找到须要批改的节点, 依据no编号 //定义一个辅助变量 Node temp = head.next; while(temp != null) { if(temp.index == newNode.index) { temp.data = newNode.data; break; } temp = temp.next; } } //删除节点 //咱们在比拟时,是temp.next.data和须要删除的节点的data比拟 public void del(String data) { Node temp = head; while (temp.next != null) { if (temp.next.data.equals(data)) { temp.next = temp.next.next; break; } temp = temp.next; //temp后移,遍历 } } //显示链表[遍历] public void list() { Node temp = head.next; while (temp != null) { System.out.println(temp.data); //将temp后移 temp = temp.next; } } //获取到单链表的节点的个数,不统计头节点 public int getLength() { int length = 0; Node temp = head.next; while (temp != null) { length ++; //将temp后移 temp = temp.next; } return length; } //查找单链表中的倒数第k个结点 //1. 编写一个办法,一个k //2. k示意是倒数第k个节点 //3. 先把链表从头到尾遍历,失去链表的总的长度 getLength //4. 失去size后,咱们从链表的第一个开始遍历 (size-k)个,就能够失去 //5. 如果找到了,则返回该节点,否则返回null public Node findLastKNode( int k) { //判断如果链表为空,返回null if(head.next == null) { return null;//没有找到 } //第一个遍历失去链表的长度(节点个数) int size = getLength(); //第二次遍历 size-k 地位,就是咱们倒数的第K个节点 //先做一个k的校验 if(k <=0 || k > size) { return null; } //定义给辅助变量, for 循环定位到倒数的index Node temp = head.next; //3 // 3 - 1 = 2 for(int i =0; i< size-k; i++) { temp = temp.next; } return temp; } //将单链表反转 public void reversetList() { //如果以后链表为空,或者只有一个节点,无需反转,间接返回 if(head.next == null || head.next.next == null) { return; } //定义一个辅助的指针(变量),帮忙咱们遍历原来的链表 Node cur = head.next; Node next = null;// 指向以后节点cur的下一个节点 Node reverseHead = new Node(0, ""); //遍历原来的链表,每遍历一个节点,就将其取出,并放在新的链表reverseHead 的最前端 while(cur != null) { next = cur.next;//先临时保留以后节点的下一个节点,因为前面须要应用 cur.next = reverseHead.next;//将cur的下一个节点指向新的链表的第一个节点 reverseHead.next = cur; //将cur 连贯到新的链表上 cur = next;//让cur后移 } //将head.next 指向 reverseHead.next , 实现单链表的反转 head.next = reverseHead.next; } //能够利用栈这个数据结构,将各个节点压入到栈中,而后利用栈的先进后出的特点,就实现了逆序打印的成果 public void reversePrint() { if(head.next == null) { return;//空链表,不能打印 } //创立要给一个栈,将各个节点压入栈 Stack<Node> stack = new Stack<Node>(); Node cur = head.next; //将链表的所有节点压入栈 while(cur != null) { stack.push(cur); cur = cur.next; //cur后移,这样就能够压入下一个节点 } //将栈中的节点进行打印,pop 出栈 while (stack.size() > 0) { System.out.println(stack.pop().data); //stack的特点是先进后出 } }}//定义Nodeclass Node { public int index;//序号 public String data;//数据 public Node next; //指向下一个节点 public Node(int index,String data) { this.index = index; this.data = data; }}双向链表双向链表的节点比单链表多一个pre节点a.减少节点思路剖析与单链表一样,借助辅助节点,找到最初一个节点的next指向新节点,但双向链表的新节点还需指向最初一个节点 ...

July 13, 2020 · 3 min · jiezi

数据结构之队列

队列是一个有序列表,能够用数组或是链表来实现。遵循先入先出的准则,即:先存入队列的数据,要先取出。后存入的要后取出。 数组实现队列数组队列思路:front=-1 指向队列头的前一个地位rear=-1 指向队列尾的数据(即就是队列最初一个数据)判断队列是否满:front ==rear 判断队列是否空:rear == maxSize-1 class ArrayQueue { private int maxSize; // 示意数组的最大容量 private int front; // 队列头 private int rear; // 队列尾 private int[] arr; // 该数据用于存放数据, 模仿队列 // 创立队列的结构器 public ArrayQueue(int arrMaxSize) { maxSize = arrMaxSize; arr = new int[maxSize]; front = -1; // 指向队列头部,剖析出front是指向队列头的前一个地位. rear = -1; // 指向队列尾,指向队列尾的数据(即就是队列最初一个数据) } // 判断队列是否满 public boolean isFull() { return rear == maxSize - 1; } // 判断队列是否为空 public boolean isEmpty() { return rear == front; } // 增加数据到队列 public void addQueue(int n) { // 判断队列是否满 if (isFull()) { System.out.println("队列满,不能退出数据~"); return; } rear++; // 让rear 后移 arr[rear] = n; } // 获取队列的数据, 出队列 public int getQueue() { // 判断队列是否空 if (isEmpty()) { // 通过抛出异样 throw new RuntimeException("队列空,不能取数据"); } front++; // front后移 return arr[front]; } // 显示队列的所有数据 public void showQueue() { // 遍历 if (isEmpty()) { System.out.println("队列空的,没有数据~~"); return; } for (int i = 0; i < arr.length; i++) { System.out.printf("arr[%d]=%d\n", i, arr[i]); } }环形队列思路:front=0 指向队列第一个元素,也就是说arr[front]就是队列第一个元素rear=0 指向队列的最初一个元素的后一个地位,因为心愿空进去一个空间作为约定判断队列是否满:(rear + 1)% maxSize = front = 0 判断队列是否空:rear == front队列中无效数据个数:(rear + maxSize - front) % maxSize ...

July 13, 2020 · 2 min · jiezi

红黑树插入与删除算法的原理及实现

红黑树在最坏状况下二叉查找树的性能非常蹩脚,咱们迫切需要一种可能所有操作都能在对数工夫内实现的数据结构。接下来咱们就来介绍一下一种十分罕用的动静保护的均衡二叉树——红黑树。 在引入红黑树之前,咱们须要理解一下2-3查找树。 2-3查找树2-3查找树的介绍 上图是一个简略的2-3查找树。能够看出,比起一般的二叉查找树,2-3查找树多了一个3-节点。3-节点的性质也是相似于2-节点的。因而,2-3查找树的查找算法与一般二叉查找树的查找算法也是相似的(只是要对3-节点探讨)。 那么,2-3树的插入算法又要怎么实现呢?何时插入2-节点,又要何时插入3-节点呢? 发明者给出了答案: 2-3查找树的插入算法依据插入元素的大小,查找到插入的地位: 如果插入的地位是一个2-节点,那么就与该节点节点造成3-节点,如下图: 如果插入的地位是一个3-节点,那么就与该节点造成一个4-节点,再从4-节点成长出一个新的2-节点 这个过程咱们能够看出2-3查找树的成长过程: 向树的底部找到地位插入元素,如果插入的地位是2-节点。2-节点会与插入元素一起成长为3-节点。如果是3-节点,那么3-节点会与插入元素造成4-节点。4-节点会向上成长,即两头的元素会与父节点联合。如果父节点是2-节点,就会造成3-节点。如果父节点是3-节点,那么就会造成4-节点持续成长。2-3查找树的性质部分变换:这是与AVL二叉均衡树最大的区别,2-3查找树只须要在部分实现2-3-4节点的变换即可。全局有序,完满均衡:部分变换不会扭转2-3树的有序性与平衡性。这是因为在插入的过程保障有序。惟一一个扭转树高度的操作——4-节点的向上成长,也是左右子树同时高度+1。在一个大小为N的2-3树中,查找和插入操作的拜访的节点必然不超过lgN个。一颗含有N个节点的2-3树的高度在$[lgN,log_3N]$之间。 红黑二叉查找树前言:替换3-节点红黑二叉查找树的根本思维就是用规范的二叉查找树(齐全由2-节点形成)和一些额定的信息(替换3-节点)来示意2-3树。由此咱们将树中的链接分为两个品种:红链接将两个2-节点连接起来形成一个3-节点,黑链接则是2-3树中的一般链接。简略来说:咱们将原来的3-节点示意为一条左斜的红色链接。 基于2-3树的性质以及红黑树的根本思维,咱们能够给出红黑树的定义: 红链接均为左链接;没有任何一个节点同时和两条红链接相连,即不存在4-节点;该树是完满彩色均衡的,即任意空链接(即指向null的节点指针)到根节点的门路上的黑链接数量雷同。满足这些定义的红黑树和相应2-3树是一一对应的。如下图: 如上图:任意空链接到根节点的黑链接都是雷同的——2条。 咱们在Node类(节点)中增加color属性,代表与父节点指向它的链接的色彩: private static final boolean RED = true;private static final boolean BLACK = false;private Node root;private class Node{ Key key; //键 Value val; //相关联的值 Node left, right; //左右子树 int N; //子树中的节点总数 boolean color; //由其父节点指向它的链接的色彩 public Node(Key key, Value val, int n, boolean color) { this.key = key; this.val = val; this.N = n; this.color = color; }}/** * 判断是否是3-节点 */private boolean isRed(Node x){ if (x == null) { return false; } return x.color == RED;}插入操作因为红黑树是二叉树构造的2-3树,因而插入操作实际上是与2-3树一样的。当看到这里时,天然就想到了两个问题: ...

July 9, 2020 · 3 min · jiezi

树形结构效率对比

二叉查找树 (Binary Search Tree)概念 二叉查找树又称二叉搜索树,二叉排序树,特点如下: 左子树上所有结点值均小于根结点右子树上所有结点值均大于根结点结点的左右子树本身又是一颗二叉查找树二叉查找树中序遍历得到结果是递增排序的结点序列。BST 的操作代价分析:查找代价:任何一个数据的查找过程都需要从根结点出发,沿某一个路径朝叶子结点前进。因此查找中数据比较次数与树的形态密切相关。 当树中每个结点左右子树高度大致相同时,树高为logN。则平均查找长度与logN成正比,查找的平均时间复杂度在O(logN)数量级上。 当先后插入的关键字有序时,BST退化成单支树结构。此时树高n。平均查找长度为(n+1)/2,查找的平均时间复杂度在O(N)数量级上。 插入代价:新结点插入到树的叶子上,完全不需要改变树中原有结点的组织结构。插入一个结点的代价与查找一个不存在的数据的代价完全相同。 删除代价:当删除一个结点P,首先需要定位到这个结点P,这个过程需要一个查找的代价。然后稍微改变一下树的形态。如果被删除结点的左、右子树只有一个存在,则改变形态的代价仅为O(1)。如果被删除结点的左、右子树均存在,只需要将当P的左孩子的右孩子的右孩子的…的右叶子结点与P互换,在改变一些左右子树即可。因此删除操作的时间复杂度最大不会超过O(logN)。 BST效率总结 :查找最好时间复杂度O(logN),最坏时间复杂度O(N)。 插入删除操作算法简单,时间复杂度与查找差不多。 平衡二叉查找树 ( Balanced Binary Search Tree )二叉查找树在最差情况下竟然和顺序查找效率相当,这是无法仍受的。事实也证明,当存储数据足够大的时候,树的结构对某些关键字的查找效率影响很大。当然,造成这种情况的主要原因就是BST不够平衡(左右子树高度差太大)。既然如此,那么我们就需要通过一定的算法,将不平衡树改变成平衡树。因此,AVL树就诞生了。 AVL 的操作代价分析:查找代价:AVL是严格平衡的BST(平衡因子不超过1)。那么查找过程与BST一样,只是AVL不会出现最差情况的BST(单支树)。因此查找效率最好,最坏情况都是O(logN)数量级的。 插入代价:AVL必须要保证严格平衡(|bf|<=1),那么每一次插入数据使得AVL中某些结点的平衡因子超过1就必须进行旋转操作。事实上,AVL的每一次插入结点操作最多只需要旋转1次(单旋转或双旋转)。因此,总体上插入操作的代价仍然在O(logN)级别上(插入结点需要首先查找插入的位置)。 删除代价:AVL删除结点的算法可以参见BST的删除结点,但是删除之后必须检查从删除结点开始到根结点路径上的所有结点的平衡因子。因此删除的代价稍微要大一些。每一次删除操作最多需要O(logN)次旋转。因此,删除操作的时间复杂度为O(logN)+O(logN)=O(2logN) AVL 效率总结 :查找的时间复杂度维持在O(logN),不会出现最差情况 AVL树在执行每个插入操作时最多需要1次旋转,其时间复杂度在O(logN)左右。 AVL树在执行删除时代价稍大,执行每个删除操作的时间复杂度需要O(2logN)。 红黑树 (Red-Black Tree )二叉平衡树的严格平衡策略以牺牲建立查找结构(插入,删除操作)的代价,换来了稳定的O(logN) 的查找时间复杂度。但是这样做是否值得呢? 能不能找一种折中策略,即不牺牲太大的建立查找结构的代价,也能保证稳定高效的查找效率呢? 答案就是:红黑树。 RBT 的操作代价分析:查找代价:由于红黑树的性质(最长路径长度不超过最短路径长度的2倍),可以说明红黑树虽然不像AVL一样是严格平衡的,但平衡性能还是要比BST要好。其查找代价基本维持在O(logN)左右,但在最差情况下(最长路径是最短路径的2倍少1),比AVL要略逊色一点。 插入代价:RBT插入结点时,需要旋转操作和变色操作。但由于只需要保证RBT基本平衡就可以了。因此插入结点最多只需要2次旋转,这一点和AVL的插入操作一样。虽然变色操作需要O(logN),但是变色操作十分简单,代价很小。 删除代价:RBT的删除操作代价要比AVL要好的多,删除一个结点最多只需要3次旋转操作。 RBT 效率总结 :查找 效率最好情况下时间复杂度为O(logN),但在最坏情况下比AVL要差一些,但也远远好于BST。 插入和删除操作改变树的平衡性的概率要远远小于AVL(RBT不是高度平衡的)。因此需要的旋转操作的可能性要小,而且一旦需要旋转,插入一个结点最多只需要旋转2次,删除最多只需要旋转3次(小于AVL的删除操作所需要的旋转次数)。虽然变色操作的时间复杂度在O(logN),但是实际上,这种操作由于简单所需要的代价很小。 B~树/B+树 (B-Tree )对于在内存中的查找结构而言,红黑树的效率已经非常好了(实际上很多实际应用还对RBT进行了优化)。但是如果是数据量非常大的查找呢?将这些数据全部放入内存组织成RBT结构显然是不实际的。实际上,像OS中的文件目录存储,数据库中的文件索引结构的存储…. 都不可能在内存中建立查找结构。必须在磁盘中建立好这个结构。那么在这个背景下,RBT还是一种好的选择吗? 在磁盘中组织查找结构,从任何一个结点指向其他结点都有可能读取一次磁盘数据,再将数据写入内存进行比较。大家都知道,频繁的磁盘IO操作,效率是很低下的(机械运动比电子运动要慢不知道多少)。显而易见,所有的二叉树的查找结构在磁盘中都是低效的。因此,B树很好的解决了这一个问题。 B-Tree的操作代价分析:查找代价:B-Tree作为一个平衡多路查找树(m-叉)。B树的查找分成两种:一种是从一个结点查找另一结点的地址的时候,需要定位磁盘地址(查找地址),查找代价极高。另一种是将结点中的有序关键字序列放入内存,进行优化查找(可以用折半),相比查找代价极低。而B树的高度很小,因此在这一背景下,B树比任何二叉结构查找树的效率都要高很多。而且B+树作为B树的变种,其查找效率更高。 插入代价:B-Tree的插入会发生结点的分裂操作。当插入操作引起了s个节点的分裂时,磁盘访问的次数为h(读取搜索路径上的节点)+2s(回写两个分裂出的新节点)+1(回写新的根节点或插入后没有导致分裂的节点)。因此,所需要的磁盘访问次数是h+2s+1,最多可达到3h+1。因此插入的代价是很大的。 删除代价:B-Tree的删除会发生结点合并操作。最坏情况下磁盘访问次数是3h=(找到包含被删除元素需要h次读访问)+(获取第2至h层的最相邻兄弟需要h-1次读访问)+(在第3至h层的合并需要h-2次写访问)+(对修改过的根节点和第2层的两个节点进行3次写访问)。 定义:一颗m阶(m>=3,即一个结点包含的数据和子节点数),3阶B-树有如下特点: 1. 根结点之多3颗子树 2. 定义: define m 3                 /*B 树的阶*/ ...

June 24, 2020 · 1 min · jiezi

Red-Black-Tree-红黑树

R-B Tree,全称Red-Black Tree,又称为红黑树,它一种特殊的二叉查找树。红黑树的每个节点上都有存储位表示节点的颜色,可以是红(Red)或黑(Black)。红黑树的特性: 节点是红色或黑色。根节点是黑色。每个叶节点(NIL节点,空节点)是黑色的。每个红色节点的两个子节点都是黑色。(从每个叶子到根的所有路径上不能有两个连续的红色节点)从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点。

June 24, 2020 · 1 min · jiezi

BTree-BTree

B-TreeB树中所有结点中孩子结点个数的最大值成为B树的阶,通常用m表示,从查找效率考虑,一般要求m>=3。一棵m阶B树或者是一棵空树,或者是满足以下条件的m叉树。 1)每个结点最多有m个分支(子树);而最少分支数要看是否为根结点,如果是根结点且不是叶子结点,则至少要有两个分支,非根非叶结点至少有ceil(m/2)个分支,这里ceil代表向上取整。 2)如果一个结点有n-1个关键字,那么该结点有n个分支。这n-1个关键字按照递增顺序排列。 3)每个结点的结构为: n k1 k2 ... kn p0 p1 p2 ... pn 其中,n为该结点中关键字的个数;ki为该结点的关键字且满足ki<ki+1;pi为该结点的孩子结点指针且满足pi所指结点上的关键字大于ki且小于ki+1,p0所指结点上的关键字小于k1,pn所指结点上的关键字大于kn。 4)结点内各关键字互不相等且按从小到大排列。 5)叶子结点处于同一层;可以用空指针表示,是查找失败到达的位置。 B+ TreeB+树是B树的变体,也是一种多路搜索树,定义基本与B树同,除了: 1.非叶子结点的子树指针与关键字个数相同; 2.非叶子结点的子树指针P[i],指向关键字值属于[K[i], K[i+1])的子树(B树是开区间); 3.为所有叶子结点增加一个链指针; 4.所有关键字都在叶子结点出现; 为什么说B+树比B树更适合数据库索引1、 B+树的磁盘读写代价更低:B+树的内部节点并没有指向关键字具体信息的指针,因此其内部节点相对B树更小,如果把所有同一内部节点的关键字存放在同一盘块中,那么盘块所能容纳的关键字数量也越多,一次性读入内存的需要查找的关键字也就越多,相对IO读写次数就降低了。 2、B+树的查询效率更加稳定:由于非终结点并不是最终指向文件内容的结点,而只是叶子结点中关键字的索引。所以任何关键字的查找必须走一条从根结点到叶子结点的路。所有关键字查询的路径长度相同,导致每一个数据的查询效率相当。 3、由于B+树的数据都存储在叶子结点中,分支结点均为索引,方便扫库,只需要扫一遍叶子结点即可,但是B树因为其分支结点同样存储着数据,我们要找到具体的数据,需要进行一次中序遍历按序来扫,所以B+树更加适合在区间查询的情况,所以通常B+树用于数据库索引。

June 24, 2020 · 1 min · jiezi

数据结构第二章线性表1线性表

线性表线性表的基本概念 线性表的定义线性表是具有相同数据类型的n(n>=0)个元素的有限序列。 线性表的基本操作 什么时候要传入参数的引用“&”?一种是值类型,使用时会直接复制原值,修改参数不会影响原值 一种是引用类型,使用时操作的是原值,修改时直接修改原值!(C语言不支持这种引用类型!) 为什么要实现对数据结构的基本操作?团队合作编程,你定义的数据结构要让别人能够很方便的使用(封装)将常用的操作/运算封装称函数,避免重复工作,降低出错风险。总结 注意⚠️:位序是用1开始计算的!!! 源码源码查看地址,点击 传送门 吧~如果你这个系列的文章有帮助到你的话,不妨给点个赞吧!那将给我莫大的动力! 同系列其他文章目录[[数据结构]第一章绪论(1)——数据结构](https://blog.csdn.net/weixin_...[[数据结构]第一章绪论(2)——算法](https://blog.csdn.net/weixin_...[[数据结构]第二章线性表(1)——线性表](https://blog.csdn.net/weixin_...[[数据结构]第二章线性表(2)——顺序表](https://blog.csdn.net/weixin_...[[数据结构]第二章线性表(3)——单链表](https://blog.csdn.net/weixin_...[[数据结构]第二章线性表(4)——双链表](https://blog.csdn.net/weixin_...[[数据结构]第二章线性表(5)——循环链表](https://blog.csdn.net/weixin_...[[数据结构]第二章线性表(6)——静态链表](https://blog.csdn.net/weixin_...[[数据结构]第二章线性表(7)——章节反思](https://blog.csdn.net/weixin_...

June 20, 2020 · 1 min · jiezi

数据结构第一章绪论2算法

算法基本概念 什么是算法?程序=数据结构+算法 算法的特性有穷性:一个算法必须总在执行有穷步之后结束,且每一步都可在有穷时间内完成。注:算法必须是有穷的,二程序可以是无穷的。 确定性:算法每一条指令必须有确切的含义,对于相同的输入只能得出相同的输出可行性:算法描述的操作都可以通过已经实现的基本运算执行有限次来实现。输入:一个算法有0个或多个输入,这些输入取自某个特定对象的集合。输出:一个算法有一个或多个输出,这些输出是与输入有着某种特定关系的量。五个特性,缺一不可 “好”算法的特质正确性:算法应能正确地解决求解问题。可读性:算法应具有良好的可读性,帮助人们理解。健壮性:输入非法数据时,算法能适当地做出反应或进行处理,而不会产生莫名其妙的输出结果。高效率与底存储量需求:执行速度快,时间复杂度低。不费内存,空间复杂度低。总结 算法效率的度量 如何评估算法时间开销?让算法先运行,事后统计运行时间? 存在的问题? 和机器性能有关,比如:超级计算机VS单片机和编程语言有关,越高级的语言执行效率越低,没错,就是越低和编译程序产生的机器指令质量有关有些算法是不能事后统计的,比如,导弹控制算法。评价一个算法优劣时,需要排除与算法本身无关的外界因素,能否事先估计? 算法时间复杂度事前预估算法时间开销T(n)与问题规模n的关系(T 表示 time) 如何计算T,例子: 问题1:是否可以忽略表达式某些部分? 加法规则:多项相加,只保留最高阶的项,且系数变为1 乘法规则:多项相乘,都保留 算法时间复杂度阶数顺序 如果有好几千行代码,需要一行一行数?顺序执行的代码只会影响常数项,可以忽略只需要挑循环中的一个基本操作,分析它的执行次数和n的关系就好如果有多层嵌套循环,只需要关注最深层的循环循环了几次小练习 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dQ6nwKRn-1592326001155)(https://tva1.sinaimg.cn/large...] 总结 算法的性能问题只有在n很大时才会暴露出来。 算法空间复杂度原地工作算法 分析空间复杂度时,只需关注与问题规模相关的变量就好(讲人话,就是,看程序中的变量就好) 加法法则 函数递归调用带来的内存开销 在这种情况下,空间复杂度等于递归调用的深度。 递归调用的过程中,每一次开辟的内存空间也可以不一致,如上例。 总结 源码源码查看地址,点击 传送门 吧~如果你这个系列的文章有帮助到你的话,不妨给点个赞吧!那将给我莫大的动力! 同系列其他文章目录[[数据结构]第一章绪论(1)——数据结构](https://blog.csdn.net/weixin_...[[数据结构]第一章绪论(2)——算法](https://blog.csdn.net/weixin_...[[数据结构]第二章线性表(1)——线性表](https://blog.csdn.net/weixin_...[[数据结构]第二章线性表(2)——顺序表](https://blog.csdn.net/weixin_...[[数据结构]第二章线性表(3)——单链表](https://blog.csdn.net/weixin_...[[数据结构]第二章线性表(4)——双链表](https://blog.csdn.net/weixin_...[[数据结构]第二章线性表(5)——循环链表](https://blog.csdn.net/weixin_...[[数据结构]第二章线性表(6)——静态链表](https://blog.csdn.net/weixin_...[[数据结构]第二章线性表(7)——章节反思](https://blog.csdn.net/weixin_...

June 20, 2020 · 1 min · jiezi

数据结构第一章绪论1数据结构

绪论第二节——算法基本概念 什么是算法?程序=数据结构+算法 算法的特性有穷性:一个算法必须总在执行有穷步之后结束,且每一步都可在有穷时间内完成。注:算法必须是有穷的,二程序可以是无穷的。 确定性:算法每一条指令必须有确切的含义,对于相同的输入只能得出相同的输出可行性:算法描述的操作都可以通过已经实现的基本运算执行有限次来实现。输入:一个算法有0个或多个输入,这些输入取自某个特定对象的集合。输出:一个算法有一个或多个输出,这些输出是与输入有着某种特定关系的量。五个特性,缺一不可 “好”算法的特质正确性:算法应能正确地解决求解问题。可读性:算法应具有良好的可读性,帮助人们理解。健壮性:输入非法数据时,算法能适当地做出反应或进行处理,而不会产生莫名其妙的输出结果。高效率与底存储量需求:执行速度快,时间复杂度低。不费内存,空间复杂度低。总结 算法效率的度量 如何评估算法时间开销?让算法先运行,事后统计运行时间? 存在的问题? 和机器性能有关,比如:超级计算机VS单片机和编程语言有关,越高级的语言执行效率越低,没错,就是越低和编译程序产生的机器指令质量有关有些算法是不能事后统计的,比如,导弹控制算法。评价一个算法优劣时,需要排除与算法本身无关的外界因素,能否事先估计? 算法时间复杂度事前预估算法时间开销T(n)与问题规模n的关系(T 表示 time) 如何计算T,例子: 问题1:是否可以忽略表达式某些部分? 加法规则:多项相加,只保留最高阶的项,且系数变为1 乘法规则:多项相乘,都保留 算法时间复杂度阶数顺序 如果有好几千行代码,需要一行一行数?顺序执行的代码只会影响常数项,可以忽略只需要挑循环中的一个基本操作,分析它的执行次数和n的关系就好如果有多层嵌套循环,只需要关注最深层的循环循环了几次小练习 总结 算法的性能问题只有在n很大时才会暴露出来。 算法空间复杂度原地工作算法 分析空间复杂度时,只需关注与问题规模相关的变量就好(讲人话,就是,看程序中的变量就好) 加法法则 函数递归调用带来的内存开销 在这种情况下,空间复杂度等于递归调用的深度。 递归调用的过程中,每一次开辟的内存空间也可以不一致,如上例。 总结

June 20, 2020 · 1 min · jiezi

数据结构

1.线性表[ 矩阵|数组|字符串|堆栈|队列 ] 1).概念 除首尾元素外,每个元素都有唯一一个直接前驱和唯一直接后继2).特点 同一性,有穷性,有序性3).存储方式 a.顺序存储(连续空间) b.链式存储 a).单链表(数据域+指针域) b).循环链表(首尾相接的链表) c).双向链表(前驱指针域+数据域+后驱指针域) d).静态链表(顺序存储数组模拟链表,设置游标模拟指针)4).栈|队列|串|数组 a.栈(限定性线性表) a).后进先出 b).栈顶入栈,出栈 c).顺序栈|链栈 b.队列(限定性线性表) a).先进先出 b).队尾入,队首出 c).链队列|循环队列 c.串(特定的线性表) a).定长顺序串|块链串 d.数组(线性表的扩充) a).三角矩阵| 带状矩阵 | 稀疏矩阵(三元组表示&十字链表) e.广义表(线性表的扩充) a).头尾链表(表结点(标志域+表头指针+表尾指针),元素结点(标志域+值域)) b).扩展线性链表(表结点(标志域+表头指针+表尾指针),原子结点(标志域+值域+表尾指针))2.树 1).表示法 a.倒置树结构 b.文氏图 c.广义表[嵌套括号](A(B(C,D)),E(F),G) d.陷入式2).二叉树 a.概念 每个结点的度都不大于2 & 每个结点的孩子结点次序不能任意颠倒 b.存储结构 a).顺序存储 | 链式存储([二叉链表]左孩子域+数据域+右孩子域) c.遍历顺序 a).先序(根左右) | 中序(左根右) | 后序(左右根)3).线索二叉树 a.结构(左孩子域+前驱结点指针+数据域+后继结点指针+右孩子域)4).哈夫曼树 a.概念 带权路径长度最短的二叉树 b.哈夫曼编码(左分支0右分支1)3.图 1).网状数据结构2).存储方法 a.邻接矩阵(数组) b.邻接表(稀疏图) a).表头结构(数据域+链域) b).弧结点结构(邻接点域+数据域+链域) c.邻接多重表 a).顶点结构(data+firstedge) b).边结点结构(mark+ivex+ilink+jvex+jlink+info) d.十字链表 a).顶点结构(data+firstin+firstout) b).弧结点结构(tailvex+headvex+hlink+tlink+info)3).遍历 a.深度优先 b.广度优先4.查找 ...

June 10, 2020 · 1 min · jiezi

排序习题课

下面哪种排序方法是从未排序序列中依次取出元素,与已排序序列中的元素作比较,将其放入已排序序列的正确位置上。(插入排序    ) 对序列15, 9, 7, 8, 20, -1, 4进行排序,若经一趟排序后的排列为9, 15, 7, 8, 20, -1, 4,则采用的是(   C  )排序。A.选择B.堆C.直接插入D.冒泡选择排序是每次选择未排序子列中最大(最小)的放到最后,显然4不是最值,所以A不对;冒泡排序是相邻两两比较,把最大的顶上去(顶到最左边或者最右边),显然边上两个元素不是最值,所以B也不对;希尔排序是先分组,然后针对组内采取插入排序,如果是希尔排序,那么9和15颠倒,20和-1也应该颠倒,所以D也不对;插入排序是从第二项开始与前面每一项比较,如果小于那一项,则插入那一项前面,C中第二项9比15小,所以放到15前面,完成一趟 对初始状态为递增序列的数组按递增顺序排序,最省时间的是插入排序算法,最费时间的是算法( 快速排序)堆排序:最好最坏都是nlogn快排:最好情况一趟比较可以划分两等份nlogn 最坏相对有序n*n插入:最好相对有序n  最坏n*n归并:最好最坏都是nlogn 将序列(Q, H, C, Y, P, A, M, S, R, D, F, X)中的关键字按升序排列,则(  F,H,C,D,P,A,M,Q,R,S,Y,X)是以第一个元素为枢轴的快速排序一趟扫描的结果。以Q为基准值,放到外面首先从后往前,到f比q小,填充到刚才q的位置 然后从前往后到y比q大,放到刚才的f的原始位置 再从后往前,d比q小,放到y原来的位置 再从前往后,到s比q大,放到d原来的位置 接下来,前后两个标志碰头了 将基准q放到刚才最后移动的s的原来位置上 快速排序方法在要排序的数据已基本有序情况下最不利于发挥其长处。快速排序的基本思想是以基准元素为中心,将待排序表分成两个子表,然后继续对子表进行划分,直到所有子表的长度为1。如果每次划分结果,两个子表长度相等,则效率最高,如果一个子表的长度为0则效率最低。对已基本有序的表以第1个为标准进行划分时,其中一个表长度将基本为0,效率最低。 设有5000个元素,希望用最快的速度挑选出前10个最大的,采用(    堆排序 )方法最好。用堆排序最好,因为堆排序不需要等整个排序结束就可挑出前10个最大元素,而快速排序和基数排序都需等待整个排序结束才能知道前10个最大元素。何为堆? 若需在O(nlog2n)的时间内完成对数组的排序,且要求排序是稳定的,则可选择的排序方法是(C)。快速排序堆排序归并排序直接插入排序 若要求尽可能快地对序列进行稳定的排序,则应选( B   )。A.快速排序 B.归并排序 C.冒泡排序“快些(希)选队(堆)”不稳定;稳定的排序算法:插入排序、冒泡排序、归并排序、基数排序直接插入,简单选择和冒泡排序都是比较慢的。 当待排序序列基本有序或个数较小的情况下,最佳的内部排序方法是(  直接插入排序   )。当待排序列基本有序时,对冒泡排序来说,若最大关键字位于序列首部,则每趟排序仅能使其“下沉”一个位置,要使其下沉到底部仍需n-1趟排序,也即时间复杂度仍为O(n^2)。而对简单选择排序来说,其比较次数与待排序列的初始状态无关,都是每次选择未排序子列中最大(最小)的放到最后;归并排序要求待排序列已经部分有序,而部分有序的含义是待排序列由若干有序的子序列组成,即每个子序列必须有序,并且其时间复杂度为O(n log2n);直接插入排序在待排序列基本有序时,每趟的比较次数大为降低,也即n-1趟比较的时间复杂度由O(n^2)降至O(n)。各种排序方法的选择:①就平均时间性能而言,快速排序最佳,其所需时间最省,但快速排序在最坏情况下的时间性能不如堆排序和归并排序。当n较大时,归并排序较堆排序省,但归并排序所需的辅助空间最大。②简单排序方法中,直接插入排序最简单,当待排序的结点已按键值“基本有序”且n较小时,则应采用直接插入排序或冒泡排序,直接插入排序比冒泡排序更快些,因此经常将直接插入排序和其他的排序方法结合在一起使用。③当n很大且键值位数较小时,采用基数排序较好;而当键值的最高位分布较均匀时,可先按其最高位将待排序结点分成若干子表,而后对各子表进行直接插入排序。 ④从方法的稳定性来比较,直接插入排序、冒泡排序、归并排序和基数排序是稳定的排序方法;而直接选择排序、希尔排序、堆排序和快速排序都是不稳定的排序方法。 内部排序方法的选择:各种排序方法各有优缺点,故在不同情况下可作不同的选择。通常需考虑的因素有:待排序的记录个数;记录本身的大小;记录的键值分布情况等。若待排序的记录个数n较小时,可采用简单排序方法 若n 较大时,应采用快速排序或堆排序。 若待排序的记录已基本有序,可采用起泡排序。 链式基数排序采用的是LSD的方法而基数排序就是借助“分配”和“收集”对单逻辑关键字进行排序的方法 排序的时间复杂度和空间复杂度快速排序的时间复杂度为O(nlogn),空间复杂度也是O(nlogn)。 归并排序的时间复杂度为O(nlogn),空间复杂度为O(n)。 堆排序的时间复杂度为O(nlogn),空间复杂度为O(1)。 选择排序的时间复杂度为O(n^2),空间复杂度为O(1)。 在分配排序时,最高位优先分配法比最低位优先分配法简单(错,两者无直接关系)基数排序是桶排序的改进,改进空间复杂度。而基数排序中,只有最低位优先分配法才能取得正确结果,但是最高位和最低位的时间复杂度一样的。 进一步了解桶排序和基数排序桶排序和基数排序均属于分配排序。分配排序的基本思想:排序过程无须比较关键字,而是通过用额外的空间来"分配"和"收集"来实现排序,它们的时间复杂度可达到线性阶:O(n)。简言之就是:用空间换时间,所以性能与基于比较的排序才有数量级的提高! 桶排序(Bucket Sort),也称箱排序 基本思想:设置若干个箱子,依次扫描待排序的记录 array[0],array[1],…,array[n - 1],把关键字等于 k 的记录全都装入到第 k 个箱子里(分配),然后按序号依次将各非空的箱子里的记录收集起来,从而完成排序。 桶排序所需要的额外空间取决于关键字的个数,若 array[0..n - 1] 中关键字的取值范围是 0 到 m - 1 的整数,则必须设置 m 个箱子。因此箱排序要求关键字的类型是有限类型,否则可能要无限个箱子。一般情况下每个箱子中存放多少个关键字相同的记录是无法预料的,故箱子的类型应设计成链表为宜。 ...

June 10, 2020 · 1 min · jiezi

图习题课2

下面4张有向图中,哪个不属于强连通图。( B)首先明确何为强连通图:对一个有向图,如果每个节点都存在到达其他任何节点的路径,那么就称它是强连通的。判定方法:任取有向图G的某结点S,从S开始进行深度优先搜索,若可以遍历G的所有结点;再将G的所有边反向,再次从S开始进行深度优先搜索,如果再次能够遍历G的所有结点,则G是强连通图,两次搜索有一次无法遍历所有结点,则G不是强连通图。此外,上述搜索可以换成广度优先搜索等其他方案。 某个无向图共有16条边,每个顶点的度均为2,则该无向图的顶点数为(  16  )每条边有两个顶点,所以总度数为16*2=32;又因为每个点度数为2,所以一共16条边 若某无向图一共有16条边,并且有3个度为4的顶点,4个度为3的顶点,其余顶点的度均小于3,则该无向图至少有多少个顶点?至多有多少个节点每条边有两个顶点,16*2=32,所以总度数为32;32-34-43=8剩余节点度均小于3,则最大为2 所以至少的就是剩余节度点均为2,即8/2=4,则得到3+4+4=11最大的就是剩余节度点均为1,是3+4+8=15 求下图中顶点0到5的最短路径是(  012435  )我们使用Dijkstra算法来求两点间最短路径: 第1 步:所有Wi初始化为无穷大∞;第2步:设0已访问;更新与0直接相连的所有结点的W;并将这些结点加入“待访问结点列表”。w1=1,w2=4;第3步:找出 中W最小的结点,设其“已访问”;即w1已访问。考察与它直接相连的未访问结点(若有与已访问节点相连的不再考察 ),累加并更新它们的W(若比所保存的W小才更新) ,并将它们加入“待访问结点列表”。w2=3,w3=8,w4=6第3步(重复此步):找出待访问中W最小的结点,设其“已访问”;w2.考察与它直接相连的、未访问结点(若有与已访问节点相连的不再考察 ),累加并更新它们的W(若比所保存的W小才更新) ,并将它们加入“待访问结点列表” 。w4=4 下列关于AOE网的叙述中,不正确的是(B)关键活动不按期完成就会影响整个工程的完成时间任何一个关键活动提前完成,那么整个工程将会提前完成所有的关键活动提前完成,那么整个工程将会提前完成某些关键活动若提前完成,那么整个工程将会提前完成首先要知道何为关键活动和关键路径,关键路径是图中的最长路径,关键路径长度代表整个工期的最短完成时间;关键活动则组成了关键路径关键活动延期完成,必将导致关键路径长度增加,即整个工期的最短完成时间增加,因此A正确。关键路径并不唯一,当有多条关键路径存在时,其中一条关键路径上的关键活动时间缩短,只能导致本条关键路径变成非关键路径,而无法缩短整个工期,因为其他关键路径没有变化,因此B项不正确。对于A,B两项要搞懂的是,当关键路径不唯一时,任何一条关键路径上的关键活动变长,都会使这条关键路径变成更长的关键路径,并且导致其他关键路径变成非关键路径,因此整个工期延长。所以某些关键活动缩短则不一定缩短整个工期。理解了A,B两项,C,D就很容易理解了 用DFS遍历一个无环有向图,并在DFS算法退栈返回时打印相应的顶点,则输出的顶点序列是( 逆拓扑有序 )DFS是一个递归算法,在遍历的过程中,先访问的点被压入栈底。拓扑有序是指如果点U到点V有一条弧,则在拓扑序列中U一定在V之前.深度优先算法搜索路径恰恰是一条弧,栈的输出是从最后一个被访问点开始输出,最后一个输出的点是第一个被访问的点.所以是逆的拓扑有序序列 下面所示的有向图,其拓扑排序的结果序列是(   )。 A.125634B.516234C.123456D. 521643第一种做法,排除法:按照拓扑排序的定义,若有向无环图中存在一条从i到j的边,则拓扑排序中i一定在j的前面。我们就看有没有j在i前边的第二种做法,每次选取极大顶点(入度为0的顶点),并把它跟它的出度一起从图中删掉 任一个有向无环图的拓扑序列的数量有(一个或多个 )。有向无环图的拓扑排序可以看成图的层序遍历,每一层的顶点可以有不同的顺序,这就造成拓扑排序序列不唯一 一个n个顶点的连通无向图,其边的个数至少为(N-1)完全图是每对顶点之间都恰连有一条边的简单图,N个端点的完全图有 n(n−1)/2 条边。 而一个n个顶点的连通无向图,其边的个数至少为 n - 1 ,即最小生成树的边数 在用邻接表表示图时,拓扑排序算法时间复杂度为(  O(n+e)  )。对有n个顶点和e条弧的有向图而言,建立求各顶点的入度的时间复杂度为O(e);建零入度顶点栈的时间复杂度为O(n);在拓扑排序过程中,若有向图无环,则每个顶点进一次栈、出一次栈,入度减1的操作在while语句中总共执行e次,所以总的时间复杂度为O(n+e)。 拓扑排序初始参数只有邻接表,所以第一步建立入度数组,因为每1入度对应一条弧,总共e条弧,建立入度数组的复杂度为O(e)。每个节点输出一次,n个节点遍历一次,时间复杂度为O(n)。(使用零入度节点栈的原因是,如果不把零入度节点入栈,每次输出时都要遍历节点。建立此栈,只需遍历一次。)然后节点入度减1的操作,也是一条弧对应一次,e条弧总共O(e)。以上总计O(n+2e)即O(n+e)。 即对每条弧要建立入度数组操作和删除操作,每个顶点要遍历一次并删除。故时间复杂度为O(n+e) 判断有向图是否存在回路,除了可以利用深度优先遍历算法,还可以利用拓扑排序深度优先搜索DFS(Depth First Search):访问方式类似于树的前序访问; 广度优先搜索BFS(Breadth First Search):访问方式类似于树的从树根出发的按层次遍历。

June 9, 2020 · 1 min · jiezi

图习题课

有 8 个结点的无向连通图最少有(7)条边无向连通图边最少的情况是生成树。n个顶点有n-1条边的无向图叫做生成树。权值最小的生成树是最小生成树。 用邻接表表示图进行深度优先遍历时,通常采用栈来实现算法,广度优先遍历使用队列。深度优先遍历:类似于树的前序遍历。从图中的某个顶点v出发,访问此顶点,然后从v的未被访问到的邻接点进行遍历,直到图中所有和v有路径相通的顶点都被访问到注:优先访问外层节点,访问到无新顶点时,会进行回退,访问未被访问过的分支顶点。广度优先遍历:类似于树的层序遍历。从图中的某个顶点w出发,让顶点w入队,然后顶点w再出队,并让所有和顶点w相连的顶点入队,然后再出队一个顶点t,并让所有和t相连但未被访问过的顶点入队……由此循环,指定图中所有元素都出队。 连通分量指的是有向图中的极大连通子图(错)有向图中称为强连通分量。连通图和连通分量都是针对无向图 图类型概念定义无向图连通图图中任意一对顶点都是连通的无向图连通分量非连通图的极大连通子图有向图强连通图任意一对顶点来回都是连通的有向图强连通分量非强连通图的极大强连通子图对于含有n个顶点e条边的连通图,利用prim算法求最小生成数的时间复杂度为(O(n²))首先,以一个结点作为最小生成树的初始结点,然后以迭代的方式找出最小生成树中各结点权重最小的边,并加到最小生成树中。(加入之后如果产生回路了就要跳过这条边,选择下一个结点。)当所有的结点都加入到最小生成树中后,就找出了这个连通图的最小生成树。首先,初始化部分所需时间为(|V|+|E|)。其次,在|V|-1次循环过程中,一方面要遍历各个顶点的邻接边,一共需要(|E|)的时间;另一方面在每次循环中查找权重最轻的边需要对所有顶点遍历一遍,每次循环需时(|V|)。因此算法的时间复杂度T(n) =(|V|+|E|) +(|E|) + (|V|-1)(|V|) =(|V|^2+|E|) =(|V|^2)。 边数较多可以用Prim,因为它是每次加一个顶点,对边数多的适用。 任何一个无向连通图(有一棵或多棵)最小生成树。无向连通图,从每一个顶点出发都会得到一棵对应的最小生成树,所以可以有很多棵,但是只有一个顶点时,就只能有一棵。 拓扑排序算法把一个无向图中的顶点排成一个有序序列。(错 )有向无环图才能进行拓扑排序。 求最短路径的 DIJKSTRA 算法的时间复杂度为 (O(n²)  )最短路径的迪杰斯特拉算法其实和最小生成树的普里姆算法相类似,原理是一样的,故时间复杂度为o(n^2). 一个活动的最早开始时间同该活动弧尾顶点的最早开始时间相同,一个事件的最迟开始时间为以该事件为尾的弧的活动最迟开始时间与该活动的持续时间的和。首先,做这题要注意几点概念:1)在AOE网络中,顶点表示事件,弧表示活动2)如果顶点A->B有弧,如果让弧表示为L,则A为L的弧尾,B为L的弧头,即有箭头的那一端叫头。 求关键路径是以拓扑排序为基础的。 (1). 求从指定源点到其余各顶点的迪杰斯特拉(Dijkstra)最短路径算法中弧上权不能为负的原因是在实际应用中无意义;(2). 利用Dijkstra求每一对不同顶点之间的最短路径的算法时间是O(n3 ) ;(图用邻接矩阵表示)(3). Floyd 求每对不同顶点对的算法中允许弧上的权为负,但不能有权和为负的回路。上面正确的是(2)。(1)迪杰斯特拉(Dijkstra)最短路径算法中将所有的顶点分为两部分:已经确定的最短路程的顶点集合 P 和不能确定的最短路径的顶点集合 Q,每一次遍历都能最少确定一个顶点,也就是说每一次遍历至少有一个顶点进入集合P。归入P集合的节点的最短路径及其长度不再变更,如果边上的权值允许为负值,那么有可能出现当与P内某点(记为a)以负边相连的点(记为b)确定其最短路径时,它的最短路径长度加上这条负边的权值结果小于a原先确定的最短路径长度,而此时a在Dijkstra算法下是无法更新的,由此便可能得不到正确的结果.。 (2)求的是一对顶点,时间复杂度跟Floyd算法一样都是O(n^3),如果求得是一个顶点到其他顶点的最短路径,时间复杂度才为O(n^2)。

June 9, 2020 · 1 min · jiezi

树习题课2

一棵有n个结点的二叉树,按层次从上到下,同一层从左到右顺序存储在一维数组A[1..n]中,则二叉树中第i个结点(i从1开始用上述方法编号)的右孩子在数组A中的位置是(  条件不足,无法确定   )必须是完全二叉树才能确定,如下图:当为完全二叉树时,为2i+1 下面几个符号串编码集合中,不是前缀编码的是(    B )。A. {0,10,110,1111}B. {11,10,001,101,0001}C. {00,010,0110,1000}D. {b,c,aa,ac,aba,abb,abc}前缀编码:前缀编码,就是哈夫曼编码,即二叉树的一种应用,用来压缩文件体积。假设一篇文章里各种单词出现次数不同,那么用不同的编码表示不同单词,可以对文件体积进行压缩。哈夫曼编码也就是前缀编码,要求是尽量减缩某些出现频率高的文字符号的编码,但必须保证任一字符编码不是另一个字符的前缀—,否则会出错。比如abcd,如果用最后一个编写a=0,b=1,c=00,d=11,那么0011就不知道是aabb还是cd了。总之,前缀编码,在一个字符集中,任何一个字符的编码都不是另一个字符编码的前缀 设森林F中有三棵树,第一,第二,第三棵树的结点个数分别为M1,M2和M3。与森林F对应的二叉树根结点的右子树上的结点个数是( M2+M3    )。 根据森林转换为二叉树的法则,二叉树的根结点通常是第一棵树的结点,二叉树的左子树是由第一棵树删去根后所得所有子树构成的,二叉树的右子树是由其它树(第二,第三棵树)构成的,故左子树结点个数是M1-1,右子树上的结点个数是M2+M3。 已知某二叉树的后序遍历序列是dabec, 中序遍历序列是debac ,  它的前序遍历是(   cedba  )。首先看后序遍历,最后的c是二叉树的根节点,然后看中序遍历,最后一个又是c,所以这个二叉树根节点没有右子树。c的位置得到后,再看后序遍历,e在c前面,所以e是c的左孩子节点,e的位置得到。然后再看中序遍历,e前面只有一个d,所以d是e的左孩子节点,d的位置得到;剩下的b和a就在e的右子树。然后再看后序遍历,d是一个叶子节点,那么就还有一个叶子节点,那么这个节点就一定是a,那么b就是e的右孩子节点,最后再结合中序遍历就可得出所表示得二叉树。总之,先根据后序遍历确定根节点(最后一个位置),再到中序遍历中分开左右子树;然后从后序遍历中确定左右子树的根节点,以此类推 引入二叉线索树的目的是( A )A. 加快查找结点的前驱或后继的速度B. 为了能在二叉树中方便的进行插入与删除C. 为了能方便的找到双亲D. 使二叉树的遍历结果唯一首先明确,对于n个结点的二叉树,在二叉链存储结构中有n+1个空链域。我们就是利用这些空链域,来存放在某种遍历次序下该结点的前驱结点和后继结点的指针,这些指针称为线索,加上线索的二叉树称为线索二叉树 在二叉树结点的先序序列,中序序列和后序序列中,所有叶子结点的先后顺序( 完全相同)遍历方法中的先序、中序、后序指的是对根的访问顺序,而对于叶子结点都采用先遍历左子树,后遍历右子树。 一棵二叉树高度为h(根的高度为1),所有结点的度或为0或为2,则这棵二叉树最少有(2h-1)个结点首先明确,n0表示度为0的根节点,即叶子节点;如果我们想撑起来一个高度为h的就只能靠n2,由于高度为h,那么我们只要h-1层每层都有一个n2就可以撑起来h的高度;又因为n0=n2+1,所以总结点个数为h-1+h 一棵左右子树均不空的二叉树在前序线索化后,其中空的链域的个数是(1)首先明确,对于n个结点的二叉树,在二叉链存储结构中有n+1个空链域。这是因为n个节点有2n个指针,而n个节点中有n-1条边(除了头结点没有边,其余节点都有一个父节点,相当于都有1条边,共n-1条),所以剩下的空链域就是2n-(n-1)=n+1,即n+1个空指针 。前序遍历最后一个结点的右指针为空;后序遍历第一个节点的左指针为空;中序遍历的第一个和最后一个节点为空。即前序和后序线索化之后,空链域为1;中序线索化后,空链域为2 利用二叉链表存储树,则根结点的右指针是(空)二叉链表又称为左孩子右兄弟存储法。根节点没有兄弟,所以为空 一棵完全二叉树上有1001个结点,其中叶子结点的个数是( 501)跟这个同一深度的满二叉树的结点数为1023,其中最后一行512个;而这个1001个少了22个,少在了最后一行,所以这缺失的22个的父结点都是叶子,共22/2=11个;而这一行剩下512-22 = 490个叶子,所以总共490+11=501个叶子结点 给定一棵树,可以找到一棵二叉树与之对应二叉树的做成是按照左子女右兄弟的规则来的,这样的方式是为了保证一棵树做成二叉树之后可以还原成那棵树。 二叉树只是作为树的更高效率的存储方式,所以为了保证树结构不会被弄乱,按照上面的逻辑,一棵树只能对应一棵二叉树 一棵一般树的结点的前序遍历和后序遍历分别与它相应二叉树的结点前序遍历和后序遍历是一致的。(   错  )树的前根遍历和对应二叉树的前序遍历相同,树的后根遍历与对应二叉树的中序遍历相同。 中序遍历一棵二叉排序树的结点就可得到排好序的结点序列一棵空树,或者是具有下列性质的二叉树:(1)若左子树不空,则左子树上所有结点的值均小于它的根结点的值;(2)若右子树不空,则右子树上所有结点的值均大于它的根结点的值;(3)左、右子树也分别为二叉排序树;(4)没有键值相等的结点。这样的树称为二叉排序树。按中序遍历二叉排序树,所得到的中序遍历序列是一个递增有序序列。 用一维数组存储二叉树时,总是以前序遍历顺序存储结点(错)一维数组存储二叉树,总是以层次遍历的顺序存储。二叉树的存储结构分为顺序存储和链式存储。 顺序存储:即用一维数组存储二叉树中的结点。采用层次遍历,从上至下从左到右依次存储,根节点存储在下标为1的位置,对于任意位置i的结点,其左子树位于2i,右子树位于2i+1,所以树结点的序号可以唯一反映出结点的逻辑关系。完全二叉树和满二叉树采用顺序存储比较合适。非完全二叉树需要添加空结点使之变成完全二叉树进行顺序存储,但是这样可能造成空间的浪费。 链式存储结构:用链表表示二叉树。每个结点存储自身的数据以及两个分别指向左孩子和右孩子结点的指针。(lchild, data, rchild) 对于有n个结点的二叉树,其高度为log2 n(错)我们先想一个极端的例子,每层只有一个结点,则深度为n。完全二叉树的高度为【logn】+1,(【】为向上取整),或者为log(2*n),两者表示等价。

June 8, 2020 · 1 min · jiezi

树习题课

哈夫曼树在一棵树中,从一个结点到另一个结点所经过的所有结点,被我们称为两个结点之间的路径。 上面的二叉树当中,从根结点A到叶子结点H的路径,就是A,B,D,H。 在一棵树中,从一个结点到另一个结点所经过的“边”的数量,被我们称为两个结点之间的路径长度。 仍然用刚才的二叉树举例子,从根结点A到叶子结点H,共经过了3条边,因此路径长度是3。 结点的带权路径长度,是指树的根结点到该结点的路径长度,和该结点权重的乘积。如上图,假设结点H的权重是3,从根结点到结点H的路径长度也是3,因此结点H的带权路径长度是 3 X 3 = 9。 树的带权路径长度(WPL),即在一棵树中,所有叶子结点的带权路径长度之和。仍然以这颗二叉树为例,树的路径长度是 3X3 + 6X3 + 1X2 + 4X2 + 8X2 = 53 哈夫曼树:哈夫曼树(Huffman Tree)是在叶子结点和权重确定的情况下,带权路径长度最小的二叉树,也被称为最优二叉树举个例子,给定权重分别为1,3,4,6,8的叶子结点,我们应当构建怎样的二叉树,才能保证其带权路径长度最小?原则上,我们应该让权重小的叶子结点远离树根,权重大的叶子结点靠近树根。 下图左侧的这棵树就是一颗哈夫曼树,它的WPL是46,小于之前例子当中的53: 以下说法错误的是(  A )A. 哈夫曼树是带权路径长度最短的树,路径上权值较大的结点离根较近。B. 若一个二叉树的树叶是某子树的中序遍历序列中的第一个结点,则它必是该子树的后序遍历序列中的第一个结点。C. 已知二叉树的前序遍历和后序遍历序列并不能惟一地确定这棵树,因为不知道树的根结点是哪一个。D. 在前序遍历二叉树的序列中,任何结点的子树的所有结点都是直接跟在该结点的之后。度为二的树就是二叉树。(  错 )首先,我们明确一个概念,树的度数是按节点的最大度数来定义的。所以,度为 2 的树要求每个节点最多只能有两棵子树,并且至少有一个节点有两棵子树;这与二叉树的要求:度不超过 2,就是说度也可以是 1 或者 0相矛盾。其次,二叉树还有一个重要特点:左子树和右子树不一样;而普通的树不分左右子树. 二叉树共有5种基本形态(1)空二叉树;(2)只有一个根结点的二叉树;(3)只有左子树;(4)只有右子树;(5)左右子树都有的二叉树 霍夫曼树的结点个数不能是偶数首先,哈夫曼树只有度为0和2的节点。这是因为哈夫曼树的构造总是以两棵值最小的树合并,每次合并都是两棵子树,不会有度为1的节点。由二叉树性质,二叉树的节点关系n0=n2+1,也就是度为0的节点数总是比度为2的节点数多1.若其中一个为奇数那么另外一个就必然是偶数,加起来就还是奇数。 哈夫曼树的构造方法假设有6个叶子结点,权重依次是2,3,7,9,18,25,如何构建一颗哈夫曼树,也就是带权路径长度最小的树呢? 第一步:构建森林 我们把每一个叶子结点,都当做树一颗独立的树(只有根结点的树),这样就形成了一个森林: 在上图当中,右侧是叶子结点的森林,左侧是一个辅助队列,按照权值从小到大存储了所有叶子结点。至于辅助队列的作用,我们后续将会看到。 第二步:选择当前权值最小的两个结点,生成新的父结点,不断重复至只有一个节点。 借助辅助队列,我们可以找到权值最小的结点2和3,并根据这两个结点生成一个新的父结点,父节点的权值是这两个结点权值之和: 此时,队列中仅有一个结点,说明整个森林已经合并成了一颗树,而这棵树就是我们想要的哈夫曼树:

June 6, 2020 · 1 min · jiezi

数组矩阵习题课

模式串T= 'abcaabbcabcaabdab' , T的next数组值及nextval数组值为(01112231123456712和01102131011021701)a b c a a b b c a b c a a b d a b maxl 0 0 0 1 1 2 0 0 1 2 3 4 5 6 0 1 2 next 0 1 1 1 2 2 3 1 1 2 3 4 5 6 7 1 2 nextval 0 1 1 0 2 1 3 1 0 1 1 0 2 1 7 0 1 ...

June 6, 2020 · 1 min · jiezi

串数组和广义表习题课

若串 S=’software’,其子串的数目是( 36)。1+2+...+8=8*9/2=36,但是值得注意的是,software没有重复字符,若有重复字符,要注意去除重复字符的情况 下面说法不正确的是(1)广义表的表头总是一个广义表广义表的表尾总是一个广义表广义表难以用顺序存储结构广义表可以是一个多层次的结构广义表第一个元素是表头,其余的元素组成的表为表尾。因此表头head可以是原子或者一个表,但表尾tail一定是一个表(即使是空表) 数组与一般线性表的区别主要是( D)。A.存储方面B. 元素类型方面C.逻辑结构方面D.不能进行插入和删除运算线性表和数组的区别:从概念上来看,线性表是一种抽象数据类型;数组是一种具体的数据结构。线性表与数组的逻辑结构不一样,线性表是元素之间具有1对1的线性关系的数据元素的集合,而数组是一组数据元素到数组下标的一一映射。并且从物理性质来看,数组中相邻的元素是连续地存储在内存中的;线性表只是一个抽象的数学结构,并不具有具体的物理形式,需要通过其它具有物理形式的数据结构来实现。在存储方面,数组中相邻的元素是连续地存储在内存中的;线性表中相邻的元素则不一定存储在连续的内存空间中,除非表是用数组来实现。对于数组,可以利用其下标在一个操作内随机存取任意位置上的元素;对于线性表,除非通过数组实现,否则只能根据当前元素找到其前驱或后继,因此要存取需要为i的元素,一般不能在一个操作内实现线性表可以动态分配元素,数组不行,不能进行插入和删除运算 常对数组进行的两种基本操作是(C )A. 建立与删除B. 索引与修改C. 查找与修改D. 查找与索引查找与索引的区别:一般查找可能要遍历所有记录,而如果建立了索引就相当为记录创建了一个目录,查找时只要到目录中搜索即可,数据量非常大的时候速度快。打个比方:如果有一本1000页的书,如果要找书中的某个内容时,在没有目录的情况下,你是否要一页一页的去翻书找呢;而如果对书建一个目录的话你只要查找目录就可以找到了。道理是一样的,对一本没有目录的书进行查找就相当于一般查找,而建了目录就相当于建了索引的记录,索引查找时只要查找索引目录就可。 串 S=‘aaab’的 Next 数组值为1123. ( 错)序号:1 2 3 4数组:a a a bnext:0 1 2 3首先注意上边序号、数组和next的对应关系求next值的过程:前两位:next数组值前两位一定为01,即aaab中的前两位aa对应01,如上表中next第1,2位为0和1.其实这就可以得出答案了.第三位:3a前面是2a(2a表示序号为2的a),2a的next数组值为1,找到序号为1的字符, 即1a,将2a和1a相比,两者相同,都是a,则3a的next值为2a的next值加1,即2;第四位:4b前3a的next为2,找到序号为2的字符, 即2a, 将3a与2a相比,二者相同,则其next值为3a的next加1,为3.结果为0123如果比较的时候碰到与前一位字符“不同”怎么办?如求下列序号4的next值序号: 1 2 3 4数组: a a b anext: 0 1 2 1以前一位的next值为序号,找到这个序号对应的字符,再进行比较,4a前一位是3b, next值为2, 找到序号2对应的2a, 比较3b和2a, 两者不同, 再重复这一步骤, 找到2a的next值是1,比较3b和1a, 不同, 那么所求的4a的next就是1, 如果相同就是2a的next值加1,为2。做个练习:xyxyyxxyx,求它的next数组?答案是011231223,你做对了吗? 串是一种数据对象和操作都特殊的线性表串的逻辑结构和线性表相似,不同之处在于串针对的是字符集,也就是串中的元素都是字符;对于串的基本操作与线性表有很大的差别。线性表更关注单个元素的操作,比如说查找一个元素,插入或者删除一个元素;但串中更多关注一部分元素的操作,如查找子串位置,得到指定子串,替换子串等操作。 在进行字符初始化时,char s[] = {’H’,’e’,’l’,’l’,’o’,’!’}是不合法的。因为没有加入字符串结束符。这个字符串的长度同样是不确定的,如果你在初始化s时变为char s[]={'h','e','l','l','o',' \0'}; 那么,他的长度应该就是5(字符串结束符不算) 数组是同类型值的集合。(错 )首先要注意概念性的错误,集合中的元素不能重复,而数组中的元素可以重复,所以,数组不能简单的说成是集合其次,虽然java,c、C++中必须要同类,但是像JS,Python,PHP这样的脚本语言大多不需要同类。 稀疏矩阵压缩存储后,必会失去随机存取功能特殊矩阵压缩后不失去随机存取功能,稀疏矩阵必定失去: 特殊矩阵指值相同的元素或零元素在矩阵中的分布有一定规律,因此可以对非零元素分配单元(对值相同元素只分配一个单元),将非零元素存储在向量中,元素的下标i和j和该元素在向量中的下标有一定规律,可以用简单公式表示,仍具有随机存取功能。而稀疏矩阵是指非零元素和矩阵容量相比很小(t<<m*n),且分布没有规律。用十字链表作存储结构自然失去了随机存取的功能。即使用三元组表的顺序存储结构,存取下标为i和j的元素时,要扫描三元组表,下标不同的元素,存取时间也不同,最好情况下存取时间为O(1),最差情况下是O(n),因此也失去了随机存取的功能。

June 6, 2020 · 1 min · jiezi

栈和队列习题课

设计一个判别表达式中括号是否配对出现的算法,采用( 栈  )数据结构最佳。解析:依次判断表达式中的每个字符,若是左括号就入栈,若是右括号则出栈,出栈的时候判断是否为空,如果为空,则说明不匹配;最后读到表达式末尾没有字符了,再判断一下栈是否为空,如果为空,则说明匹配,不为空,说明不匹配。 若一个栈的输入序列为1,2,3,…,n,输出序列的第一个元素是i,则第j个(1≤j≤i)输出元素是( 不能确定 )。解析:不能确定是一次性全进完、全出完,只知道进入的序列不变。当一次性弄完时,答案为i-j+1;当边进边出时,元素难以确定。 修改一下此题目,若一个栈的输入序列为1,2,3,.....,n,输回出序列的第一个元素是n,则第i个输出元素(一定是n-i+1)。因为暗含的意思就是,从1到n-1已经全部入栈,输出的顺序就是从n到1的唯一顺序,第i个输出元素就为n-i+1 栈在( 递归调用、子程序调用、表达式求值 )中应用。解析:栈就是一个先进后出的结构;队列就是一个先进先出的结构。一般只要满足这个特点就可以称之为栈或队列。栈的应用:非常广泛,一般在计算机中,只要数据的保存满足先进后出的原理,都优先考虑使用栈,是计算机中不可缺的机制。在CPU内部就有提供栈这个机制。主要用途:函数调用和返回,数字转字符,表达式求值,走迷宫,子程序调用和返回,中断时数据保存和返回。在编程语言中主要用来进行函数的调用和返回。队列的应用:队列主要用在和时间有关的地方,特别是操作系统中,队列是实现多任务的重要机制。windows中的消息机制也是通过队列来实现的。进程调度也是使用队列来实现,所以队列也是一个重要的机制。只要满足数据的先进先出原理就可以使用队列。 向一个顺序栈插入一个元素时,首先把待插入元素写入到这个位置上,然后使栈顶后移一个位置。 ( 错 )解析:插入先判满,弹出先判空。1、进栈(PUSH)算法①若TOP≥n时,则给出溢出信息,作出错处理(进栈前首先检查栈是否已满,满则溢出;不满则作②);②置TOP=TOP+1(栈指针加bai1,指向进栈地址);③S(TOP)=X,结束(X为新进栈的元du素);2、退栈(POP)算法①若TOP≤0,则给出下溢信息,作zhi出错处理(退栈前先检查是否已为空栈, 空则下溢;不空则作②);②X=S(SOP),(退栈后的元素赋给X);③TOP=TOP-1,结束(栈dao指针减1,指向栈顶)。 假设以数组A[m]存放循环队列的元素,其头尾指针分别为front和rear,则当前队列中的元素个数为(  (rear – front + m)% m  )解析:首先注意,rear指向最后一个元素的下一个元素,所以rear-front即可,不需要+1;其次+m是为了避免当rear 已经转了一圈后,rear的值小于 front的值的情况,保证分子大于0 用链接方式存储的队列,在进行删除运算时(  头、尾指针可能都要修改   )。在有头结点的链队列的出队操作中,一般只需修改队头指针;但当原队列中只有一个结点时,该结点既是队头也是队尾,故删去此结点时亦需修改队尾指针,使其指向头结点,且删去此结点后队列变空 在一个循环队列Q中,判断队空的条件为Q.rear+1 == Q.front,队满的条件为(Q.rear+1)%MAXSIZE==Q.front每次从队列中取出的应是具有最高优先权的元素,这种队列就是优先级队列。队列是一种特征为FIFO的数据结构,每次从队列中取出的是最早加入队列中的元素。但是,许多应用需要另一种队列,每次从队列中取出的应是具有最高优先权的元素,这种队列就是优先级队列(Priority Queue),也称为优先权队列。优先级队列是0个或多个元素的集合,每个元素都有一个优先权或值。当给每个元素分配一个数字来标记其优先级时,可设较小的数字具有较高的优先级,这样更方便地在一个集合中访问优先级最高的元素,并对其进行查找和删除操作。对优先级队列,执行的操作主要有:(1)查找,(2)插入,(3)删除。在最小优先级队列(min Priority Queue)中,查找操作用来搜索优先权最小的元素,删除操作用来删除该元素。在最大优先级队列(max Priority Queue)中,查找操作用来搜索优先权最大的元素,删除操作用来删除该元素。插入操作均只是简单地把一个新的元素加入到队列中 消除递归不一定需要使用栈不是所有的递归转化为非递归都要用到栈。转化为非递归主要有两种方法:对于尾递归或单向递归,可以用循环结构算法代替;其余的使用栈的方法。例如求阶乘的,是单向递归,直接用循环去替代从1乘到n就是结果了;斐波那契数列亦然 任何一个递归过程都可以转换成非递归过程即使对不含相同元素的同一输入序列进行两组不同的合法的入栈和出栈组合操作,所得的输出序列也一定相同(错)解释一下题目的意思:对于同一个输入序列(序列中元素各不相同),使用两种不同的入栈和出栈组合操作,所得到的输出序列一定相同。 解析:对于栈来说,由于输入序列中元素各不相同,所以输出序列肯定不同;如果是使用两种不同的(合法)的入队和出队组合操作,则其输出序列一定是相同的。

June 6, 2020 · 1 min · jiezi

发现数据结构之美栈

什么是栈? 数据结构图入栈出栈图JavaScript中的Array与栈 在js中,如何发现出栈LIFO的特性?如何实现一个最小栈?leetcode 栈 解法题目 20.有效的括号(easy)67.二进制求和(easy)905.按奇偶排序数组(easy)56.合并区间(medium)75.颜色分类(medium)228.汇总区间(medium)739.每日温度(medium)面试题 栈 解法题目 实现一个BigInt什么是栈?栈是一种后入先出(LIFO)的数据结构栈通常使用DFS(Depth First Search)遍历通常可以通过top/bottom来代表栈的顶部和底部数据结构图 入栈出栈图 JavaScript中的Array与栈在js中,如何发现出栈LIFO的特性?下面这个结构大家都熟悉,瞬间体现出栈LIFO的特性。 // 定义一个栈let stack = [1,2,3];// 入栈stack.push(4); // stack [1,2,3,4]// 出栈stack.pop(); // 4 stack [1,2,3]如何实现一个最小栈?设计一个支持 push ,pop ,top 操作,并能在常数时间内检索到最小元素的栈。 push(x) —— 将元素 x 推入栈中。pop() —— 删除栈顶的元素。top() —— 获取栈顶元素。getMin() —— 检索栈中的最小元素。var MinStack = function() { this.stack = [];};MinStack.prototype.push = function(x) { this.stack.push(x);};MinStack.prototype.pop = function() { return this.stack.pop();};MinStack.prototype.top = function() { return this.stack[this.stack.length - 1];};MinStack.prototype.getMin = function() { return Math.min(...this.stack);};题目:https://leetcode-cn.com/probl...解法:https://github.com/FrankKai/l... ...

June 4, 2020 · 4 min · jiezi

重学数据结构之链表篇

本文是重新数据结构系列文章的第二篇,本文和大家一起探讨链表的相关知识。@[toc] 链表是怎么样的数据结构链表,不需要连续的内存空间,通过“指针(引用)”将一组零散的内存块串联起来的数据结构。内存块在链表中也叫“结点”,每个结点除了存储数据,还需要记录链上的下一个或者上一个结点的地址。 链表的特点1.插入、删除数据效率高O(1)级别(只需更改指针指向即可),随机访问效率低O(n)级别(需要从链头至链尾进行遍历)。2.和数组相比,内存空间消耗更大,因为每个存储数据的节点都需要额外的空间存储后继指针。 常见的链表结构单链表1.只有一个方向,每个结点只存储下一个结点的地址,记录下一个结点的指针成为后继指针(next)2.有两个特殊结点,头结点和尾结点。头结点用来记录链表的基地址;尾结点指向一个空地址NULL,表示链表的最后一个结点3.链表的插入和删除操作,因为不考虑内存空间的连续性,只需要关注相邻结点的指针变化,所以时间复杂度为O(1)4.链表的随机访问操作,因为内存空间的不连续性,需要指针一个结点一个结点的依次访问,直到找到对应的结点,所以时间复杂度为O(n) 双向链表1.有两个方向,每个结点既有指向后面结点的后继指针next,也有指向前面结点的前驱指针prev, 因为需要同时存储前后两个指针,因此双向链表占用更多的内存存储空间2.首节点的前驱指针prev和尾节点的后继指针均指向空地址。3.对于删除、插入操作可以实现比单链表更加高效的O(1)。对于删除操作,一般就是如下两种情况: a.删除结点中“值等于某个给定值”的结点;b.删除给定指针指向的结点。对于第一种情况,无论单链表还是双向链表,都需要从头结点开始每个结点依次进行遍历,直到找到值等于某个给定值对应结点,进行删除,对于单纯的删除操作,时间复杂度为O(1),但是对于遍历查找结点的操作时间负责就是O(n),所以根据时间复杂度分析中的加法法则,第一种情况下链表操作的总时间复杂度为 O(n)。 而对于第二种情况,双向链表结点存储了前驱指针prev,直接就可以找到对应结点进行指针操作删除,所以其时间复杂度为O(1);而单链表因为没有前驱指针,依然需要从头开始遍历结点,直到p->next=q。说明p是q的前结点,因此这种情况下单链表的删除操作时间复杂度为O(n) 4.对于查询操作,双线链表也比单链表高效,因为我们可以记录上次上次的位置,再查询时只需要查询一半即可 5.LinkedHashMap的底层实现就是用的双向链表结构。 循环链表1.循环链表是一种特殊的单链表,2.尾结点指针是指向链表的头结点3.处理环形结构数据时,使用用循环链表,比如注明的约瑟夫问题。 链表or数组从时间复杂度分析性能数组因为其需要内存空间的连续性,符合CPU的缓存机制,所以访问效率更高。数组的缺点:1.大小固定,申请需要整块的连续空间,如果空间不足,可能申请失败2.无法动态扩容,如果申请空间不够,需要申请更大的空间,需要数据复制拷贝进入新的数组,非常耗时;而链表天然支持动态扩容,因为他不要内存空间的连续性链表的缺点:1.需要更多的内存空间来存储指向下一节点的指正2.频繁的插入、删除操作,会导致内存的频繁申请和释放,容易造成内存碎片,Java中容易触发系统GC(Garbage Collection,垃圾回收)机制 链表的应用链表的经典应用场景就是LRU 缓存淘汰算法。缓存是一种提高数据读取性能的技术,在硬件设计、软件开发中都有着非常广泛的应用,比如常见的 CPU 缓存、数据库缓存、浏览器缓存等等。缓存大小是有限制的,在缓存空间满的时候,就需要使用一下策略进行清理,常见的策略有三种:先进先出策略 FIFO(First In,First Out)、最少使用策略 LFU(Least Frequently Used)、最近最少使用策略 LRU(Least Recently Used)。如何基于链表实现LRU 缓存淘汰算法?首先,维护一个有序的单链表,规定越靠近尾部的数据时间越早,处理数据时:1.如果数据已存在链表中,遍历结点,找到对应结点的数据进行删除,插入链表头部,时间复杂度为O(n)2.如果数据不再链表中,分两种情况 如果此时缓存未满,则将此结点直接插入到链表的头部,时间复杂度为O(1);如果此时缓存已满,则链表尾结点删除,将新的数据结点插入链表的头部,时间复杂度为O(1)。正确写出链表的6个技巧理解指针或引用的含义警惕指针丢失和内存泄漏利用哨兵简化实现难度重点留意边界条件处理举例画图、辅助思考多写多练觉得文章不错的,给我点个赞哇,关注一下呗! 技术交流可关注微信公众号【君伟说】,加我好友一起探讨 微信交流群:加好友(备注技术交流)邀你入群,抱团学习共进步

June 4, 2020 · 1 min · jiezi

数组实现环形队列Java

用数组实现环形队列的特点是高效。 能快速判断队列是否 满/空;能快速存取数据。因为简单高效,所以甚至在硬件中都实现了环形队列。        环形队列广泛应用于网络数据的收发,和不同应用间数据交换(内核和应用程序大量交换数据,从硬件接受大量数据) 内存上没有环形结构,因此环形队列实际上用数组的线性空间来实现。       但是当数据到了尾部如何处理呢?它将转回到0位置来处理。这个转回是通过取模操作来执行的。       因此,环形队列是逻辑上将数组元素q[0]和q[maxSize - 1]连接,形成一个存放队列的环形空间。 为了方便读写,还要用数组下标来指明队列的读写位置。head/tail.其中head指向可以读的位置,tail指向可以写的位置。 环形队列的关键是判断队列为空,还是为满。当tail追上head时,队列为满时,当head追上tail时,队列为空。但如何知道谁追上谁。还需要一些辅助的手段来判断.    如何判断环形队列为空,为满有两种判断方法。 一.附加一个标志位tag 当head赶上tail,队列空,则令tag=0,当tail赶上head,队列满,则令tag=1,二.限制tail赶上head,即队尾结点与队首结点之间至少留有一个元素的空间。 队列空:   head==tail队列满:   (tail+1)% MAXN ==headpackage ds.queue;/** * 用数组实现环形队列 * 附加标志位法 */public class ArrayCircleQueue { private int[] arr; private int maxSize; private int rear; private int front; // 当front赶上rear,说明队列满,令flag=0; // 当rear赶上front,说明队列空,令flag=1 private int flag; public ArrayCircleQueue(int size) { maxSize = size; arr = new int[maxSize]; front = 0; rear = 0; flag = 1; } public boolean isFull() { return front == rear && flag == 0; } public boolean isEmpty() { return front == rear && flag == 1; } public int peek() { if (isEmpty()) throw new RuntimeException("queue is empty!!!"); return arr[front]; } public void offer(int v) { if (isFull()) { System.out.println("queue is full, could not add."); return; } arr[rear] = v; rear = (rear + 1) % maxSize; if (rear == front) flag = 0; } public int poll() { if (isEmpty()) throw new RuntimeException("queue is empty!"); int res = arr[front]; front = (front + 1) % maxSize; if (front == rear) flag = 1; return res; } public void show() { if (isEmpty()) { System.out.println("The queue is empty!!!"); return; } System.out.println("front -> rear"); for (int i = front; i < front + size(); i++) { System.out.print(arr[i % maxSize] + " "); } System.out.println(); } public int size() { if (rear > front) return rear - front; return rear + maxSize - front; }}package ds.queue;/** * 数组实现环形队列 * -预留空间法 */public class ArrayCircleQueue2 { private int maxSize; private int front; // 指向队列的第一个元素,arr[front]就是队列的第一个元素。初始值0 private int rear; // 指向队列的最后一个元素的后一个位置,希望空出一个空间作为约定。初始值0 private int[] arr; public ArrayCircleQueue2(int size) { maxSize = size; arr = new int[size]; } public boolean isFull() { return (rear + 1) % maxSize == front; } public boolean isEmpty() { return front == rear; } public void offer(int v) { if (isFull()) { System.out.println("queue is full!!!"); return; } arr[rear] = v; rear = (rear + 1) % maxSize; } public int poll() { if (isEmpty()) throw new RuntimeException("queue is empty!!!"); int res = arr[front]; front = (front + 1) % maxSize; return res; } public int size() { return (rear + maxSize - front) % maxSize; } public int peek() { if (isEmpty()) throw new RuntimeException("queue is empty"); return arr[front]; } public void show() { if (isEmpty()) { System.out.println("queue is empty!!!"); return; } System.out.println("Front -> rear"); for (int i = front; i < front + size(); i++) { System.out.print(arr[i % maxSize]); } System.out.println(); }}测试代码 ...

June 1, 2020 · 3 min · jiezi

二分查找算法详解

二分查找[toc] 最基本的二分查找int binarySearch(int[] nums, int target) { int left = 0; int right = nums.length - 1; // 注意 while(left <= right) { int mid = left + (right - left) / 2; //注意:如果用(right + left) / 2 会出现数值过大溢出现象 if(nums[mid] == target) return mid; else if (nums[mid] < target) left = mid + 1; // 注意 else if (nums[mid] > target) right = mid - 1; // 注意 } return -1;}边界问题为什么 while 循环的条件中是 <=,而不是 <?因为初始化 right 的赋值是 nums.length - 1,即最后一个元素的索引,而不是 nums.length。这二者可能出现在不同功能的二分查找中,区别是:前者相当于两端都闭区间 [left, right],后者相当于左闭右开区间 [left, right),因为索引大小为 nums.length 是越界的。 ...

June 1, 2020 · 6 min · jiezi

单链表反转面试官你确定要问这个吗

前言:单链表是一种常见、重要的数据结构,并且随着时间飞逝,也衍生出了诸多针对单链表的操作算法,例如,今天本文中即将会聊到的单链表的反转操作 。下面会结合一些图片详细讲解下单链表的数据结构,以及通过三种方式(递归、双指针法、循环遍历)进行单链表的反转。 数据结构:1、单链表的数据结构:单链表是一种线性结构,它是由一个个节点(Node)组成的。并且每个节点(Node)是由一块数据域(data)和一块指针域(next)组成的。      ①、节点(Node)结构图如下: 节点的数据域:data数据域一般就是用来存放数据的 。(注:data域需要指定类型,只能存放指定类型的数据,不能什么东西都放,是不是呀; 那代码中是怎么实现的呢? 使用 泛型 。)节点的指针域:next指针域一般就是存放的指向下一个节点的指针;这个指针其实是一个内存地址,因为Node节点对象是存放在JVM中的堆内存中,所以节点的next指针域中存放就是下一个节点在堆内存中的地址;而在代码中对象的内存地址是赋值给其引用变量了,所以指针域中存放的是下一个节点对象的引用变量。 ②、单链表结构图如下:(下图是由三个节点构成的单链表) 若有所思,en en en . . . . . . 好像单链表的知识在脑海中清晰了些呀;那要不我们快马加鞭,赶紧把单链表的数据结构代码弄出来,然后再思索下怎么进行反转, en en en. . . .. . . 嘿嘿! 代码:1、Node节点类:创建Node节点类,节点类中并且额外提供了两个方法(单链表的创建方法、单链表的遍历放歌);注意:单链表的创建方法 createLinkedList( ):Node节点的插入方式为 尾插法 , 其实还有 头插法 方式; 扩展:链表中节点的插入方式还在 HashMap 中使用到了,在 JDK 1.7 时是头插法,JDK 1.8时是尾插法; /** * @PACKAGE_NAME: com.lyl.linklist * @ClassName: Node 节点类 * @Description: 单链表的组成元素:Node节点 * @Date: 2020-05-30 16:18 **/public class Node<T> { // 节点的数据域 public T data; // 节点的指针域 public Node next; /** * 构造方法 * @param data 数据域值 */ public Node(T data) { this.data = data; } /** * 创建 单链表 (尾插法) * @return 返回头结点 */ public static Node createLinkedList(){ // 头节点 Node<String> head; Node<String> n = new Node<String>("111"); Node<String> n1 = new Node<String>("222"); Node<String> n2 = new Node<String>("333"); // 指定头节点 head = n; n.next = n1; n1.next = n2; // 返回头结点 return head; } /** * 链表遍历 * @param node */ public static void traverse(Node node) { while (node != null) { System.out.print(node.data + " --> "); node = node.next; } System.out.print("null"); System.out.println(); }}2、单链表反转:①、递归实现反转:/** * @PACKAGE_NAME: com.lyl.linklist * @ClassName: ReverseByRecursiveTest * @Description: 使用递归实现单链表反转 * @Date: 2020-05-30 17:01 **/public class ReverseByRecursiveTest { /** * 使用 递归 实现单链表反转 * @param head 链表的 头节点 * @return 返回反转后的 head 头结点 */ public static Node reverse(Node head) { if (head == null || head.next == null) { return head; } // 获取头结点的下个节点,使用temp临时节点存储 Node temp = head.next; // 递归调用 Node node = reverse(head.next); // 将头节点的下一个节点的指针域指向头节点 temp.next = head; // 将头节点的指针域置为null head.next = null; return node; } // test public static void main(String[] args) { // 创建单链表 Node head = Node.createLinkedList(); // 遍历新创建的单链表 System.out.print("新创建的单链表: "); Node.traverse(head); // 递归反转单链表 Node newHead = reverse(head); // 遍历反转后的单链表 System.out.print("反转后的单链表: "); Node.traverse(newHead); }}运行输出:新创建的单链表: 111 --> 222 --> 333 --> null反转后的单链表: 333 --> 222 --> 111 --> null ...

May 30, 2020 · 3 min · jiezi

数据结构和算法-第一部分第三课算法复杂度上

作者 谢恩铭,公众号「程序员联盟」。转载请注明出处。原文:https://www.jianshu.com/p/149...《数据结构和算法》全系列内容简介算法的正确性算法的复杂度“渐近”度量第一部分第四课预告1. 算法的正确性上一课 数据结构和算法 | 第一部分第二课:小鸭子们去旅行 中,我们讲了一个有趣的小故事,就是为了引出算法复杂度。 算法复杂度非常重要,要讲的内容也很多,所以我们分为上下两课。当程序员需要解决计算机科学相关的问题时,他们(通常)会编写一个程序。这个程序包含一个实现,也就是说需要把算法用一种编程语言来实现。 我们知道,算法只是对解决问题的步骤的一种精确描述,它并不依赖于程序员所用的编程语言或工作环境。 我们在 数据结构和算法 | 第一部分第一课:什么是数据结构和算法 里介绍了一个“煮方便面”的食谱,这份食谱虽然简单,但可以说是一个算法。 我们是用中文来描述这份食谱的,如果我现在把这份食谱翻译成一门外语(比如 英语),但食谱的内容还是不变,只不过换了一种语言来说明罢了。换个英国人来照着这份英文食谱煮方便面,跟我做出来的会是一样的。 那么,当程序员需要把算法用一种编程语言来实现时,需要做什么呢? 像农夫 Oscar 一样,他必须首先验证他的算法是正确的,也就是说它产生了预期的结果,解决了所涉及的问题。这非常重要(如果算法不正确,那我们根本没有选择和优化它的必要),有时验证算法正确性是非常难的一步。 算法正确性,英语是 Algorithm Correctness(algorithm 表示“算法”,correctness 表示“正确性”)。要证明算法的正确性,有许多方法,我们本课程就不详述了,因为这不是我们的重点。大家有兴趣可以去网上搜索一下。 当然,算法正确了,并不能保证实现这个算法的程序就没有错误(bug)。 一旦有了一个正确的算法,我们用编程语言去实现它(通过编写一个执行它的程序)。但我们可能会写出有很多小错误的程序,这些小错误也许和所使用的编程语言有关,可能在编写程序的过程中被引入。因为算法中一般不会描述如何避免犯错,例如如何管理程序的内存,如何检查段错误(Segmentation Fault),等等,但这些都是程序员实现算法时需要考虑的问题。 2. 算法的复杂度一旦程序员确信他的算法是正确的,他将尝试评估其效率,比如他会想知道“这个算法快不快”。 有人可能会认为,要知道算法快不快的最佳方式是实现该算法并在电脑上进行测试。 有趣的是,通常情况并非如此。例如,如果两个程序员实现了两种不同的算法,并在各自的电脑上测试程序运行的快慢。那么拥有更快速度的电脑的那个程序员可能会错误地认为他的算法更快,但其实并不一定。 而且,要实现一个算法并测试,有时候并不容易。且不说有时候根据一个算法来写出实现的代码很难,如果要实现的算法涉及到一枚火箭的发射,难道每次用一枚真的火箭来发射一下去测试算法快不快吗?我们又不像钢铁侠那么有钱可以任性。 出于这些原因,计算机科学家们发明了一个非常方便而强大的概念,也就是我们接下来要学习的:算法的复杂度。 “复杂度”(Complexity,表示“复杂”,是一个名词。它对应的形容词是 complex,表示“复杂的”)这个词有点误导人,因为我们这里不是强调“理解起来有困难”(很复杂,很难),而是指效率(efficiency)。 “复杂度”并不意味着“复杂的程度”。有的算法理解起来很难(很复杂),它的复杂度却可以非常低。 如果要用一句话来简单说明算法复杂度,那可以是:“如果给实现了这个算法的程序一个大小为 N 的输入,那么这个程序将执行的操作的数目的数量级是 N 的一个怎么样的函数( f(N) )呢?” f(N) 中的 f 是 function(函数)的意思,相信以前数学课的时候,大家都学过(比如我们以前很常见的 y = f(x) )。f(N) 表示“N 的函数”。 上面那句话基于以下事实:解决问题的程序取决于问题的起始条件。如果起始条件改变,程序执行的时间也会变长或变短。 复杂度可以量化(通过数学公式)起始条件与算法执行的时间之间的关系。上面这几句话乍看有点难以理解。什么是操作?什么是操作的数目,什么是操作的数目的数量级? 不用担心,我们慢慢讲解: 复杂度的学习中会涉及一些数学的概念。所以嘛,学好数学对编程还是很有帮助的,英语同样很重要。如果你英语和数学比较好,学起编程来会轻松很多。可以参看我以前的一篇文章:对于程序员, 为什么英语比数学更重要? 如何学习 。 数量级是指数量的尺度或大小的级别,每个级别之间保持固定的比例。通常采用的比例有 10,2,1000,1024, e(欧拉数,大约等于 2.71828182846 的超越数,即自然对数的底)。-- 摘自 百度百科为了“计算操作的数目”,我们必须首先定义什么是操作。操作其实不难理解,比如第一部分第一课中,我们讲了一个“煮方便面”的算法,其中的步骤是这样的: ...

May 27, 2020 · 2 min · jiezi

最简单的动态数据结构-链表

链表是什么?上几篇文章中,分别介绍了动态数组,栈和队列,其中都是通过resize的方法进行动态扩容,从而实现了动态的数据结构,不过这种方法称为伪动态。真正的动态数据结构还要从链表说起,链表是真正的动态数据结构,一个动态数据结构不需要处理固定容量的问题,而且是最简单的动态数据结构,学好链表很重要,对后面复杂的数据结构是一种铺垫。链表见名知意,元素和元素之间就像链子一样拴住,元素也可以成为节点(Node),叫啥都行,反正能理解就好。每一个元素有两部分组成,一部分是元素本身,另一部分是下一个元素的引用。下文将元素本身就叫e,他的下一个引用就叫做next。 class Node<E>{ E e; //当前元素本身 Node next; //下一个元素的引用}上图表示,第一个元素的e是1,他的next是2。第二个元素的e是2,他的next是3。最后一个元素的e是3,但next为null,因为3就是最后一个元素,可以说如果一个元素的next为null,就代表他是最后一个元素。如果我想找到2,那就通过1去找,如果我想找到3,那不好意思,你也得通过1去找,因为1和3之间没有直接的关联,只能先通过1先找到2,2里面有存储3的地址。这也代表链表没有随机访问的能力,比如数组可以通过索引去找,效率很高。链表想找一个元素只能从头到尾遍历去找,查询效率低。 链表的实现首先创建了一个Node内部类,目的是只给LinkedList外部类用,外面的类是不可以操作Node对象的,这样做是比较安全的。成员变量分别是E e和Node next,e用于存储元素本身,next存储下一个元素的引用。LinkedList里面维护了两个变量,Node head和size,head用于存储目前链表的头元素,size用于维护链表里元素的个数。 public class LinkedList<E> { //内部维护一个Node对象,给外部类(LinkedList)使用 private class Node<E> { public E e; //当前Node对象的元素值 public Node next; //当前Node对象下一个对象的引用 public Node(E e,Node next) { this.e = e; this.next = next; } public Node(E e) { this.e = e; this.next = null; } public Node() { this.e = null; this.next = null; } } //头元素节点 private Node head; //当前链表节点个数 private int size; public LinkedList() { head = null; size = 0; } public int getSize() { return size; } public boolean isEmpty() { return size == 0; }}添加一个头节点添加头节点就是要创建一个新Node节点,新节点的e为666,新节点的next就指向了原来的头节点,改变指向后,要维护一下head,让新创建的这个节点变成头节点。最后维护一下size的个数。 ...

November 5, 2019 · 3 min · jiezi

Java中常见的数据结构

1、数据结构有什么作用?当使用 Java 里面的容器类时,你有没有想过,怎么 ArrayList 就像一个无限扩充的数组,也好像链表之类的。很好使用,这就是数据结构的用处,只不过你在不知不觉中使用了。 数据结构内容比较多,细细的讲解也是相对费功夫的,不可能达到一蹴而就。我就将常见的数据结构:堆栈、队列、数组、链表和红黑树 给大家介绍一下,作为数据结构的入门,了解一下它们的特点即可。 2、常见的数据结构数据存储的常用结构有:栈、队列、数组、链表和红黑树。栈栈:stack,又称堆栈,它是运算受限的线性表,其限制是仅允许在标的一端进行插入和删除操作,不允许在其他任何位置进行添加、查找、删除等操作。简单的说:采用该结构的集合,对元素的存取有如下的特点: 先进后出(存进去的元素,要在后它后面的元素依次取出后,才能取出该元素)。栈的入口、出口的都是栈的顶端位置。需要注意的 2 个名词 压栈:就是存元素。即,把元素存储到栈的顶端位置,栈中已有元素依次向栈底方向移动一个位置。弹栈:就是取元素。即,把栈的顶端位置元素取出,栈中已有元素依次向栈顶方向移动一个位置。队列队列:queue,简称队,它同堆栈一样,也是一种运算受限的线性表,其限制是仅允许在表的一端进行插入,而在表的另一端进行删除。简单的说,采用该结构的集合,对元素的存取有如下的特点: 先进先出(存进去的元素,要在后它前面的元素依次取出后,才能取出该元素)。队列的入口、出口各占一侧。栈和队列的结构图: 数组数组:Array,是有序的元素序列,数组是在内存中开辟一段连续的空间,并在此空间存放元素。简单的说,采用该结构的集合,对元素的存取有如下的特点: 查找元素快:通过索引,可以快速访问指定位置的元素增删元素慢 指定索引位置增加元素:需要创建一个新数组,将指定新元素存储在指定索引位置,再把原数组元素根据索引,复制到新数组对应索引的位置。 指定索引位置删除元素:需要创建一个新数组,把原数组元素根据索引,复制到新数组对应索引的位置,原数组中指定索引位置元素不复制到新数组中。 数组的结构示意图: 链表链表:linked list,由一系列结点node(链表中每一个元素称为结点)组成,结点可以在运行时i动态生成。 每个结点包括两个部分:一个是存储数据元素的数据域, 一个是存储下一个结点地址的指针域。 链表结构有单向链表与双向链表。 采用该结构的集合,对元素的存取有如下的特点: 多个结点之间,通过地址进行连接。查找元素慢:想查找某个元素,需要通过连接的节点,依次向后查找指定元素。增删元素快: 增加元素:只需要修改连接下个元素的地址即可。删除元素:只需要修改连接下个元素的地址即可。红黑树二叉树:binary tree ,是每个结点不超过2的有序树(tree) 。 简单的理解,就是一种类似于树的结构,只不过每个结点上都最多只能有两个子结点。 二叉树是每个节点最多有两个子树的树结构。顶上的叫根结点,两边被称作“左子树”和“右子树”。 结构示意图: 二叉树的一种比较有意思的叫做红黑树,红黑树本身就是一颗二叉查找树,将节点插入后,该树仍然是一颗二叉查找树。也就意味着,树的键值仍然是有序的。 红黑树的约束:节点可以是红色的或者黑色的根节点是黑色的叶子节点(特指空节点)是黑色的每个红色节点的子节点都是黑色的任何一个节点到其每一个叶子节点的所有路径上黑色节点数相同红黑树的特点:速度特别快,趋近平衡树,查找叶子元素最少和最多次数不多于二倍。

November 4, 2019 · 1 min · jiezi

Java数据类型总结

Java 中提供了八种数据类型:6个数字类型(四个整数型,两个浮点型)、字符类型、布尔型。 依次分别是 : byte、int、short、long、float、double、char、boolean。 byte类型数据占1个字节,8位,最高位表示符号,则表示范围是:-2^7~2^7-1int类型数据占4个字节,共32位,最高位表示符号,则表示范围是:-2^31~2^31-1short类型数据占2个字节,共16位,最高位表示符号,则表示范围是:-2^15~2^15-1long类型数据占8个字节,共64位,最高位表示符号,则范围是:-2^63~2^63-1float数据类型占4个字节,共32位,范围是:10^-38~10^38和-10^38~-10^-38double数据类型占8个字节,共64位,范围是:10^-308~10^308和-10^308~-10^-308char数据类型占2个字节,但最高位不用来表示符号。用它能表示 Unicode 集里的 0~2^16-1(0~65535) 位置的字符char数据类型应用有以下方式: 如:char a='A'; char a=97(不能用short类型,因为最高位表示符号,故表示数值的只有15位); 具体的可以参考下图: 2、数据类型转换时出现的问题2.1、自动转换一个 int 类型变量和一个 byte 类型变量进行相加,运算的结果是:变量的类型将是 int 类型。 这就是出现了数据类型的自动类型转换现象。 数据类型自动转换:将取值范围小的类型自动转换为取值范围大的类型。 同样道理,当一个 int 类型变量和一个 double 变量运算时, int 类型将会自动提升为 double 类型进行运算。 代码示例:public class DataDemo { public static void main(String[] args) { int a = 1; byte b = 2;// byte c = a + b;// int类型和byte类型运算,结果是int类型 int c = a + b; System.out.println(c); }}转换原理图解: 转换规则:范围小的类型向范围大的类型提升:byte、short、char 运算时候直接提升为 int。 ...

November 4, 2019 · 2 min · jiezi

无重复字符的最长子串Python3

提出问题:给定一个字符串,请你找出其中不含有重复字符的最长子串的长度。 示例:"abbcc" --> "ab" or "bc"、"abcadef" --> "adef"解决思路:使用蛮力算法算法很容易实现,但是时间复杂度为O(n^2)。本题可以通过一次遍历完成,比如"abcadef",正常遍历,如果当前遍历字符不在字符串中,将它添加至字符串;当遍历到第2个a时,发现重复,此时截取第1个a后的第一个字符开始到第2个a为新字符串,并比较最大长度,依此类推。 a --> ab --> abc -a重复-> bca --> bcad --> bcade --> bcadef代码如下( ̄▽ ̄): class Solution: def lengthOfLongestSubstring(self, s: str) -> int: if len(s) == 0: return 0 st = '' length = 0 for i in range(len(s)): if s[i] not in st: st+=s[i] length=length if(length>len(st)) else len(st) else: st = st[st.find(s[i])+1:] + s[i] return length时间与空间复杂度占用: 原题链接:https://leetcode-cn.com/probl...

October 7, 2019 · 1 min · jiezi

PAT测试题目-QQ帐户的申请与登陆

梗述与开发环境本文的重点是针对测试点1、测试点2未通过,存在的问题进行总结。编程语言:C语言。数据结构:Hash表。 测试点的问题与解决方法测试点1与测试2:运行时错误。原因:在程序中使用了strcpy函数。解决方法:自己编程实现一个类似于strcpy的函数。 测试点2:N和L指令各一半,随机交错。帐号随机,取到上下界。密码随机,取到上下界。答案错误。原因:若一个字符串的长度为n,则存储它的字符数组的长度至少为n+1。所以存储密码的字符数组大小至少为17。如果设置为16,不能通过本测试点。 其它需要注意的点:字符数组不能仅仅是定义它,还要对它初始化,置空。否则,存放输出结果的字符数组,在打印输出时,会带有一些古怪的尾巴带着输出出来。 参考文献[1] https://zhidao.baidu.com/ques... (C语言试题: 若一个字符串的长度为n,则存储它的字符数组的长度至少为n+1。)[2] https://zhidao.baidu.com/ques... (一个字符数组中包含了一个长度为n的字符串,则该字符串首尾字符的数组下标分别是什么?)[3] https://pintia.cn/problem-set... (QQ帐户的申请与登陆)

October 6, 2019 · 1 min · jiezi

为什么MySQL数据库索引选择使用B树

数据库经常存放了大量的数据,数据库的查询也是经常发生的。 当我们要查询一个庞大的数据表中的一行小小的数据的时候,就像茫茫人海中找到一个对的人一样困难... 我们为了节约时间成本,我们一定要想办法以最快的速度找到我们想要的数据。 学过数据结构的童鞋一定第一个想到:搜索树,其平均复杂度是log(n),具有不错的查询性能。 好的,故事从一个简单的二叉树开始...... 1、二叉查找树(1)二叉树简介: 二叉查找树也称为有序二叉查找树,满足二叉查找树的一般性质,是指一棵空树具有如下性质: 1、任意节点左子树不为空,则左子树的值均小于根节点的值; 2、任意节点右子树不为空,则右子树的值均大于于根节点的值; 3、任意节点的左右子树也分别是二叉查找树; 4、没有键值相等的节点; 上图为一个普通的二叉查找树,按照中序遍历的方式可以从小到大的顺序排序输出:2、3、5、6、7、8。二叉树的查找平均时间复杂度是O(log(n))。 大家看上图,如果我们的根节点选择是最小或者最大的数,那么二叉查找树就完全退化成了线性结构。 虽然我们已经拥有了平均O(log(n))的时间复杂度,但是,人类是贪心的。我们接受不了在极端情况下时间复杂度退化成O(n)。 思来想去,极端情况下这棵树已经失去了平衡,如果我们让他恢复平衡,岂不是很美妙。那就给他取个名字叫:平衡二叉树。 2、红黑树红黑树就是平衡二叉树的一种。 红黑树在每个节点增加一个存储位表示节点的颜色,可以是red或black。通过对任何一条从根到叶子的路径上各个节点着色的方式的限制,红黑树确保没有一条路径会比其它路径长出两倍。 红黑树的性质: 1、每个节点非红即黑; 2、根节点是黑的; 3、每个叶节点(叶节点即树尾端NULL指针或NULL节点)都是黑的; 4、如果一个节点是红的,那么它的两儿子都是黑的; 5、对于任意节点而言,其到叶子点树NULL指针的每条路径都包含相同数目的黑节点; 6、每条路径都包含相同的黑节点; 注意: 性质 3 中指定红黑树的每个叶子节点都是空节点,而且并叶子节点都是黑色。但 Java 实现的红黑树将使用 null 来代表空节点,因此遍历红黑树时将看不到黑色的叶子节点,反而看到每个叶子节点都是红色的。 性质 4 的意思是:从每个根到节点的路径上不会有两个连续的红色节点,但黑色节点是可以连续的。 因此若给定黑色节点的个数 N,最短路径的情况是连续的 N 个黑色,树的高度为 N - 1;最长路径的情况为节点红黑相间,树的高度为 2(N - 1) 。 性质 5 是成为红黑树最主要的条件,后序的插入、删除操作都是为了遵守这个规定。 (1)红黑树的左旋右旋红黑树的左右旋是比较重要的操作,左右旋的目的是调整红黑节点结构,转移黑色节点位置,使其在进行插入、删除后仍能保持红黑树的 5 条性质。 左旋 // 左旋代码 __leftRotate(x) { // x是要将要旋转到左子树的节点 if (x != null) { // y是将要旋转到父节点位置的节点 var y = x.right; // 把y的左节点移到x的右节点 把x的右儿子改为y的左儿子 x.right = y.left; if (y.left != null) { // 因为要移动了y的左节点 它成为了x的儿子 所以要把y.left 的爸爸改为x y.left.parent = x; } // x 的爸爸变成了 y 的爸爸 y.parent = x.parent; if (x.parent == null) { // 如果 x 是根节点了 那么记得把root 换成y this.root = y; } else if (x.parent.left == x) { // 如果 x 自身原本是 左儿子 则把x 的父亲的左儿子 换成 y x.parent.left = y; } else { // 如果 x 自身原本是 右儿子 则把p 的父亲的右儿子 换成 y x.parent.right = y; } // 把 x 变成 y 的左儿子 y.left = x; // 把 x 的 父亲变成 y x.parent = y; } }右旋 ...

October 1, 2019 · 3 min · jiezi

单向循环链表解决约瑟夫环问题-Golang-实现

问题描述编号为 1, 2, … , n 的 n 个人按顺时针方向围坐一圈,每人持有一个密码(正整数)。一开始任选一个正整数作为报数上限值 m ,从第一个人开始按顺时针方向自 1 开始顺序报数,报到 m 时停止报数。报 m 的人出列,将他的密码作为新的 m 的值,从他在顺时针方向上的下一个人开始重新从 1 报数,如此下去,直至所有人全部出列为止。 基本要求利用单向循环链表存储结构模拟此过程,按照出列的顺序印出各人的编号。 测试数据7 个人的密码依次为:3, 1, 7, 2, 4, 8, 4 ;m 值为 6 (正确的出列顺序应为 6, 1, 4, 7, 2, 3, 5)。 开始定义指针和结构体package mainimport "fmt"// 定义结构体type Person struct { num int // 序号 code int // 密码 next *Person}var head, tail *Person // 分别指向 ring 的头和尾添加操作/* 向 ring 中添加一个编号为 num ,密码为 code 的人 */func Add_Jos(ring *Person, num int, code int) { current := &Person{code: code, num: num} if head == nil { // 当 ring 中还没有人时,使 head 和 tail 指向 current head = current current.next = head tail = current } else { // 将 current 作为 ring 的尾巴,设定 current.next 为 head tail.next = current current.next = head tail = current }}移除操作/* 移除报到 code 的人,打印出这个人的序号并返回他的密码 */func Remove_Jos(ring *Person, code int) int { if code == 1 { // 当要移除的人是 head 指向的人时 code = head.code fmt.Printf("%d ", head.num) head = head.next tail.next = head // head 的指向改变,重新设定 tail.next 为 head return code } // 当要移除的人不是 head 指向的人时 for i := 0; i < code-2; i++ { // 使 head 指向需要移除的人的前一个人 head = head.next } code = head.next.code fmt.Printf("%d ", head.next.num) tail = head // 此时 head 指向的人作为新的 tail head = head.next.next // head 指向需要移除的人的下一个人,他作为新的 head tail.next = head // head 的指向改变,重新设定 tail.next 为 head return code}执行操作/* 执行操作,m 为设定的初值 */func Run_Jos(ring *Person, m int) { code := m for head != tail { // 当 head 和 tail 不指向同一个人时( ring 中剩余人数大于 1 ) code = Remove_Jos(ring, code) // 进行移除操作并获取新的密码 } fmt.Print(head.num) // 将 ring 中的最后一个人的序号打印出来}主函数func main() { var ring *Person // 添加测试数据 Add_Jos(ring, 1, 3) Add_Jos(ring, 2, 1) Add_Jos(ring, 3, 7) Add_Jos(ring, 4, 2) Add_Jos(ring, 5, 4) Add_Jos(ring, 6, 8) Add_Jos(ring, 7, 4) Run_Jos(ring, 6)}总结单向循环链表与单向链表差别并不大,只是增加了一个尾部指向头部的步骤。这个例子中使用到了 head 和 tail 两个指针用来记录 ring 中的头和尾,这样方便了向其中添加新的数据,而且头尾两个指针也有效减少了在删除过程中循环遍历结点的操作。 ...

September 19, 2019 · 2 min · jiezi

关于Java-HashMap的那些你不清楚的的事情

大热的《阿里巴巴Java开发规约》中有提到: 【推荐】集合初始化时,指定集合初始值大小。说明:HashMap使用如下构造方法进行初始化,如果暂时无法确定集合大小,那么指定默认值(16)即可:public HashMap (int initialCapacity) { this(initialCapacity, DEFAULT_LOAD_FACTOR);}看到代码规约这一条的时候,我觉得是不是有点太 low 了,身为开发,大家都知道 HashMap 的原理。 什么?这个要通过插件监测?没必要吧,哪个开发不知道默认大小,何时 resize 啊,然后我和孤尽打赌随机咨询几位同学以下几个问题: HashMap 默认bucket数组多大?如果new HashMap<>(19),bucket数组多大?HashMap 什么时候开辟bucket数组占用内存?HashMap 何时扩容?抽样调查的结果出乎我的意料: HashMap 默认bucket数组多大?(答案是16,大概一半的同学答错)如果new HashMap<>(19),bucket数组多大?(答案是32,大多被咨询同学都不太了解这个点)HashMap 什么时候开辟bucket数组占用内存?(答案是第一次 put 时,一半同学认为是 new 的时候)HashMap 何时扩容?(答案是put的元素达到容量乘负载因子的时候,默认16*0.75,有1/4同学中枪)HashMap 是写代码时最常用的集合类之一,看来大家也不是全都很了解。孤尽乘胜追击又抛出问题:JDK8中 HashMap 和之前 HashMap 有什么不同? 我知道 JDK8 中 HashMap 引入了红黑树来处理哈希碰撞,具体细节和源代码并没有仔细翻过,看来是时候对比翻看下 JDK8 和 JDK7 的 HashMap 源码了。 通过对比翻看源码,先说下结论: HashMap 在 new 后并不会立即分配bucket数组,而是第一次 put 时初始化,类似 ArrayList 在第一次 add 时分配空间。HashMap 的 bucket 数组大小一定是2的幂,如果 new 的时候指定了容量且不是2的幂,实际容量会是最接近(大于)指定容量的2的幂,比如 new HashMap<>(19),比19大且最接近的2的幂是32,实际容量就是32。HashMap 在 put 的元素数量大于 Capacity LoadFactor(默认16 0.75) 之后会进行扩容。JDK8在哈希碰撞的链表长度达到TREEIFY_THRESHOLD(默认8)后,会把该链表转变成树结构,提高了性能。JDK8在 resize 的时候,通过巧妙的设计,减少了 rehash 的性能消耗。存储结构JDK7 中的 HashMap 还是采用大家所熟悉的数组+链表的结构来存储数据。 ...

September 10, 2019 · 1 min · jiezi

前端该如何准备数据结构和算法

一、导读据我了解,前端程序员有相当一部分对“数据结构”和“算法”的基础概念都不是很清晰,这直接导致很多人在看到有关这部分的内容就会望而却步。 实际上,当你了解了“数据结构”和“算法”存在的真正意义,以及一些实际的应用场景,对它有了一个整体的认知之后,你可能会对它产生强烈的兴趣。当然,它带将带给你的收益也是相当可观的。 很多前端同学在看到“数据结构”和“算法”后会有一定的抵触心理,或者尝试去练习,但是被难倒,从而放弃。 这很大一部分原因是因为你还不够了解学习他们的意义,或者没有掌握合理的练习方法。 实际上,当你有了一定的目的性,并且有了合理的练习方法,再来学习这部分内容会变得得心应手。å在本文中,我就来分享一下我学习“数据结构”和“算法”的一些经验和方法。 后面我也会针对所有常见的数据结构和算法分类,进行全方位的梳理。 1.1 类别说明数据结构和算法的种类非常之多,拿树举例,树的种类包括:二叉树、B树、B+树、Trie树、红黑树等等,本文只选择了二叉树。 对前端来讲,没有必要对某些比较偏的类型和解法多做了解,一是浪费宝贵的时间,二是应用的不多。 本文选择的数据结构和算法的类别均是出现频率最高,以及应用最广的类别。 1.2 题目说明另外,做题时找对典型题目非常重要,可以让你更快速更高效的掌握知识,本文后面也会给出每种类型的典型题目供大家参考。 题目来源: awesome-coding-js:我的前端算法开源项目,包括我做过的题目以及详细解析leetcode剑指offer 另外,我会在后面长期更新一个前端算法的专栏,对每类数据结构和算法进行详细的讲解,敬请期待。 二、为什么要学习数据结构和算法在学习某块内容之前,我们一定要首先明确为什么要学,而不是盲目的跟风。 这将更有利于你从学习的过程中获得收益,而且会为你的学习带来动力。 首先明确一点,学习数据结构和算法不一定就是记住二叉树、堆、栈、队列等的解题方法也不是死记硬背一些题目,如果你仅仅停留在这样的表面思想,那么你学习起来会非常痛苦。 2.1 解决问题的思想计算机只是一个很冰冷的机器,你给他下发什么样的指令,它就能作出什么样的反应。 而开发工程师要做的是如何把实际的问题转化成计算机的指令,如何转化,来看看《数据结构》的经典说法: 设计出数据结构, 在施加以算法就行了。所以,很重要的一点,数据结构和算法对建立解决问题的思想非常重要。 如果说 Java 是自动档轿车,C 就是手动档吉普。数据结构呢?是变速箱的工作原理。你完全可以不知道变速箱怎样工作,就把自动档的车子从 A 开到 B,而且未必就比懂得的人慢。写程序这件事,和开车一样,经验可以起到很大作用,但如果你不知道底层是怎么工作的,就永远只能开车,既不会修车,也不能造车。如果你对这两件事都不感兴趣也就罢了,数据结构懂得用就好。但若你此生在编程领域还有点更高的追求,数据结构是绕不开的课题。2.2 面试这是非常现实的一点,也是很多前端学习数据结构和算法的原因。 一般对待算法的态度会分为以下几类: Google、Microsoft等知名外企在面试工程师时,算法是起决定性因素的,前端工程师也是一样,基本是每一轮都会考察,即使你有非常强的背景,也有可能因为一两道算法答的不好而与这样的企业失之交臂。 第二类,算法占重要因素的,国内的某些大厂在面试时,也会把数据结构和算法作为重要的参考因素,基本是面试必考,如果你达不到一定的要求,会直接挂掉。 第三类,起加分作用,很多公司不会把数据结构和算法作为硬性要求,但是也会象征性的出一些题目,当你把一道算法题答的很漂亮,这绝对是加分项。 可见,学好数据结构和算法对你跳槽更好的公司或者拿到更高的薪水,是非常重要的。 三、如何准备了解了数据结构和算法的重要性,那么究竟该用什么样的方法去准备呢? 3.1 全方位了解在学习和练习之前,你一定要对数据结构和算法做一个全方位的了解,对数据结构和算法的定义、分类做一个全面的理解,如果这部分做的不好,你在做题时将完全不知道你在做什么,从而陷入盲目寻找答案的过程,这个过程非常痛苦,而且往往收益甚微。 本文后面的章节,我会对常见的数据结构和算法做一个全方位的梳理。 3.2 分类练习当你对数据结构和算法有了一个整体的认知之后,就可以开始练习了。 注意,一定是分类练习!分类练习!分类练习!重要的事情说三遍。 我曾见过非常多的同学带着一腔热血就开始刷题了,从leetcode第一题开始,刚开始往往非常有动力,可能还会发个朋友圈或者沸点什么的????,然后就没有然后了。 因为前几题非常简单,可能会给你一定的自信,但是,按序号来的话,很快就会遇到hard。或者有的人,干脆只刷简单,先把所有的简单刷完。 但是,这样盲目的刷题,效果是非常差的,有可能你坚持下来,刷了几百道,也能有点效果,但是整个过程可能非常慢,而且效果远远没有分类练习要好。 所谓分类练习,即按每种类别练习,例如:这段时间只练习二叉树的题目,后面开始练习回溯算法的题目。 在开始练习之前,你往往还需要对这种具体的类别进行一个详细的了解,对其具体的定义、相关的概念和应用、可能出现的题目类型进行梳理,然后再开始。 3.3 定期回顾和总结在对一个类型针对练习一些题目之后,你就可以发现一定的规律,某一些题目是这样解,另一些题目是那样解...这是一个很正常的现象,每种类型的题目肯定是存在一定规律的。 这时候就可以开始对此类题目进行总结了,针对此类问题,以及其典型的题目,发现的解题方法,进行总结。当下次你再遇到这种类型的题目,你就能很快想到解题思路,从而很快的解答。 所以,当你看到一个题目,首先你要想到它属于哪种数据结构或算法,然后要想到这是一个什么类型的问题,然后是此类问题的解决方法。 如果你看到一个新的问题还不能做到上面这样,那说明你对此类题目的掌握程度还不够,你还要多花一些经历来进行练习。 当然,后面我会把我在这部分的总结分享出来,帮助大家少走一些弯路。 3.4 题目的选择关于题目来源,这里我推荐先看《剑指offer》,然后是leetcode,《剑指offer》上能找到非常多的典型题目,这对你发现和总结规律非常重要。看完再去刷leetcode你会发现更加轻松。 关于难度的选择, 这里我建议leetcode简单、中等难度即可,因为我们要做的是寻找规律,即掌握典型题目即可,当你掌握了这些规律,再去解一些hard的问题,也是可以的,只是多花些时间的问题。切忌不要一开始就在很多刁钻古怪的问题上耗费太多时间。 经过上面的方法,我在练习一段时间后,基本leetcode中等难度的问题可以在20min内AC,另外在最近跳槽的过程中,基本所有的算法问题我都能很快的手写出来,或者很快的想到解题思路。希望大家在看到我的经验和方法后也能达到这样的效果,或者做的比我更好。 四、时间复杂度和空间复杂度在开始学习之前,我们首先要搞懂时间复杂度和空间复杂度的概念,它们的高低共同决定着一段代码质量的好坏: 4.1 时间复杂度一个算法的时间复杂度反映了程序运行从开始到结束所需要的时间。把算法中基本操作重复执行的次数(频度)作为算法的时间复杂度。 没有循环语句,记作O(1),也称为常数阶。只有一重循环,则算法的基本操作的执行频度与问题规模n呈线性增大关系,记作O(n),也叫线性阶。 常见的时间复杂度有: O(1): Constant Complexity: Constant 常数复杂度O(log n): Logarithmic Complexity: 对数复杂度O(n): Linear Complexity: 线性时间复杂度O(n^2): N square Complexity 平⽅方O(n^3): N square Complexity ⽴立⽅方O(2^n): Exponential Growth 指数O(n!): Factorial 阶乘 ...

August 20, 2019 · 2 min · jiezi

TypeScript版算法与数据结构栈

本文的源码在这里,可以参考一下栈也是一种使用非常广泛的线性数据结构,它具有后进先出last in first out的特点。就像我们平时一本一本的往桌上放书,等到我们又想用书时,我们首先接触到的总是我们最后一本放上去的书。 栈的添加和删除操作总是在栈的一端完成,这一端被称为栈顶,对于栈的实现,我会采用上一篇中实现的数组作为底层的数据结构,所有栈的操作都会在这个底层的数组中完成。栈的主要操作有两个,一个是入栈,一个是出栈 首先我们会定义一个TypeScript接口,这个接口会包含基础的栈这个类的实现 interface Stack<E> { getSize(): number; // 获取栈中元素的个数 isEmpty(): boolean; // 判断栈是否为空 push(e: E): void; // 入栈一个元素 pop(): E; // 出栈一个元素 peek(): E; // 查看栈顶元素}再来看看我们类的实例属性和构造函数,其中MyArray就是上一篇中实现的数组,初始状态下,我们依然会给栈一个初始的容量10。 class ArrayStack<E> implements Stack<E> { private array: MyArray<E>; constructor(capacity: number = 10) { this.array = new MyArray<E>(capacity); }}1.入栈在有之前数组的基础上,入栈也是一个非常简单的过程,实际上底层的操作就是往数组的末尾添加一个元素。时间复杂度是O(1) public push(e: E): void { this.array.addLast(e);} 2.出栈在有之前数组的基础上,出栈也非常简单,实际上的底层操作就是删除数组中的最后一个元素,并返回该元素。出栈也是一个时间复杂度为O(1)的操作 public pop(): E { return this.array.removeLast();} 更多相关数据结构,可以前往我的github。持续更新中,喜欢的话给个star~相关的问题也可以提issue,看到后会第一时间回复

July 16, 2019 · 1 min · jiezi

TypeScript版算法与数据结构数组

本文的源码在我的github,可以参考一下数组是数据结构中最简单,也是使用最广泛的一种。在原生的js中,数组给我们提供了很多方便的操作方法,比如push(), pop(), shift(), unshift()。但出于对数据结构的学习,我将不依赖这些方法,只是使用数组简单的存储元素功能,另外我会假定数组是定长数组。这样也方便我们计算相关的时间复杂度。另外我会使用TypeScript实现,主要是因为TypeScript的强类型控制,以及泛型这些高级特性。 先来看我们自己实现数组的实例属性以及构造函数,我们用capacity来表示数组的容量,在用户没有传入容量的情况下,会给数组一个默认的capacity。我会用size来表示当前数组中元素的个数。 class MyArray<T> { private data: Array<T>; private size: number = 0; constructor(capacity = 10) { this.data = new Array(capacity); }}1.在数组中插入元素在数组index位置插入元素是我们经常使用的一个操作,那我们就需要从之前数组中index位置开始,每个元素向后移动一个位置。以便给新插入的元素挪出index这个位置。在操作的末尾,我们需要维护一下数组的size。 public add(index: number, e: T) { if (index < 0 || index > this.size) { throw new Error('Add failed. Required index >= 0 and index <= size.'); } if (this.size === this.data.length) { this.resize(2 * this.data.length); } for (let i = this.size - 1; i >= index; i--) { this.data[i + 1] = this.data[i]; } this.data[index] = e; this.size++;} ...

July 16, 2019 · 2 min · jiezi

LeetCode-攻略-2019-年-7-月上半月汇总

Create by jsliang on 2019-07-15 11:54:45 Recently revised in 2019-07-15 15:25:25一 目录不折腾的前端,和咸鱼有什么区别 | 目录 || --- | | 一 目录 | | 二 前言 || 三 汇总 || 3.1 LeetCode 已攻略 || 3.2 Function & Object || 四 总结 | 二 前言返回目录自 2019-05-16 开始,jsliang 每天折腾一道及以上 LeetCode 题目,并将其解题思路记录成文章,发布到 GitHub 和 微信公众号。 微信公众号记录截图: GitHub 记录截图: 目前关于这块 LeetCode(算法与数据结构) 的安排: 2019/08/15 前。LeetCode 简单难度题目 - 完成 100 道简单 LeetCode 题目的题解。2019/08/15 - 2019/09/15。初步钻研算法与数据结构。时间未知。LeetCode 中等难度题目 - 完成 50 道中等 LeetCode 题目的题解。时间未知。进一步钻研算法与数据结构。时间未知。LeetCode 困难难度题目 - 完成 20 道困难 LeetCode 题目的题解。时间未知。完善算法与数据结构。截至目前为止,jsliang 在攻略 LeetCode 中的收获: ...

July 15, 2019 · 2 min · jiezi

算法第一课学习笔记

一、哈佛大学智商测试(离散数学)1. 题目皇帝不是穷人,在守财奴之中也有穷人,所以,有一些( )并不是( )。 A. 皇帝,皇帝B. 守财奴,守财奴C. 守财奴,皇帝D. 皇帝,守财奴2. 解答这题可以采用离散数学中的逻辑推理来做,首先,假设下列命题为真: p: 这个人是皇帝q: 这个人是穷人r: 这个人是守财奴皇帝不是穷人: p → ¬q在守财奴之中也有穷人: ∃x(x∈r ⋀ x∈q)然后,根据p → ¬q得到其逆否命题q → ¬p也为真, 此时,由∃x(x∈r ⋀ x∈q)和q → ¬p,根据假言三段论可推知∃x(x∈r ⋀ x∈¬p), 翻译之后就是:存在一些人是守财奴且不是皇帝。 故而此题选C,有一些守财奴并不是皇帝。 3. 相关知识:假言三段论假言三段论又称假言推理。假言推理总是以假言判断为前提来进行推理的。在逻辑运算符记号中表示为: p → qq → r所以 p → r经典例子: 人都是要死的,苏格拉底是人,所以苏格拉底是要死的。参见: Hypothetical syllogism - Wikipedia 二、天平和硬币1. 题目现在有16枚外形相同的硬币,其中一枚是假币,且己知假币比真币重量轻。先给定一架没有砝码的天平,问至少需要多少次称量才能找到这枚假币? 思考:给出一种称量方案容易,但如何证明这种方法用到的称量次最少呢? 2. 解答可以采取三分法,将硬币分为三堆: A:5枚B:5枚C:6枚先称量A和B,若A(或B)较轻,则可以通过将5枚硬币再分为2枚、2枚、1枚三堆来称量,最多需要两次称量找出假币。 若A和B平衡,则假币在剩下的六枚硬币中,将其分为2枚、2枚、2枚三堆,天平两端各放两枚,无论平或不平,最多只需要两次称量就能找出假币。 综上,至少需要称量3次才能找到假币。 3. 理论下界既然一次天平称量能得到左倾、右倾、平衡三种情况,若把一次称量当成一位编码,该编码是3进制的,则该问题转换为:用多少位编码能够表示16呢? 解答:假设需要n位,则3^n >= 16,两边取对数得到n >= 2.52,这表示至少3次才能找到该轻质的假币。 三、链表相加1. 题目给定两个链表,分别表示两个非负整数,它们的数字按逆序存储在链表中,且每个结点只存储一个数字,计算两个数的和,并且返回和的链表头指针。 ...

July 10, 2019 · 3 min · jiezi

10分钟详解高级数据结构优先队列图前缀树分段树以及树状数组

优秀的算法往往取决于你采用哪种数据结构,那么在这节课里,我们将重点介绍几种高级的数据结构,它们是:优先队列、图、前缀树、分段树以及树状数组。 之所以称它们为高级的数据结构,是因为它们的实现要比那些常用的数据结构要复杂得多,这些高级的数据结构能够让你在处理一些复杂问题的过程中多拥有一把利器。同时,掌握好它们的性质以及所适用的场合,在分析问题的时候回归本质,那么很多题目都能迎刃而解了。 下面我们就来一一介绍它们。 优先队列(Priority Queue)图片: https://uploader.shimo.im/f/l... 首先我们来介绍一下优先队列。 -- 用图来说明 和普通队列不同的是,优先队列最大的作用是能保证每次取出的元素都是队列中优先级别最高的,这个优先级别可以是自定义的,例如,数据的数值越大,优先级越高,或者是数据的数值越小,优先级越高,优先级别甚至可以通过各种复杂的计算得到。 优先队列最常用的场景是从一堆杂乱无章的数据当中按照一定的顺序(或者优先级)逐步地筛选出部分的乃至全部的数据。 例如,任意给定一个数组,要求找出前k大的数。试想一下,最直接的办法就是先对这个数组进行排序,然后依次输出前k大的数,这样的复杂度将会是O(nlogn),其中,n是数组的元素个数。 有没有更好的办法呢?如果我们借用优先队列,就能将复杂度优化成O(k + nlogk),当数据量很大(即n很大),而k相对较小的时候,显然,利用优先队列能有效地降低算法复杂度,其本质就在于,要找出前k大的数,我们并不需要对所有的数进行排序。现在问题来了,这个复杂度是如何计算出来的呢?要理解它,我们先来看看优先队列的实现方法。 优先队列的本质是一个二叉堆结构,堆在英文里叫Binary Heap,它是利用一个数组结构来实现的完全二叉树。换句话说,优先队列的本质是一个数组,数组里的每个元素既有可能是其他元素的父节点,也有可能是其他元素的子节点,而且,每个父节点只能有两个子节点,这就很像一棵二叉树的结构了。这里有三个重要的性质需要牢记:数组里的第一个元素array[0]拥有最高的优先级别。 给定一个下标 i,那么对于元素 array[i] 而言:它的父节点所对应的元素下标是 (i-1) / 2它的左孩子所对应的元素下标是 2*i + 1它的右孩子所对应的元素下标是 2*i + 2 数组里每个元素的优先级别都要高于它两个孩子的优先级别。 图片: https://uploader.shimo.im/f/7... 优先队列最基本的操作就是两个: 向上筛选(sift up / bubble up)向下筛选(sift down / bubble down) 什么是向上筛选呢?当有新的数据加入到优先队列中,新的数据首先被放置在二叉堆的底部,然后不断地对它进行向上筛选的操作,即如果发现它的优先级别比父节点的优先级别还要高,那么就和父节点的元素相互交换,再接着网上进行比较,直到无法再继续交换为止。由于二叉堆是一棵完全二叉树,并假设堆的大小为k,因此整个过程其实就是沿着树的高度网上爬,所以只需要O(logk)的时间。 3 / \ 5 9 3, 5, 9 3 / \ 5 9 3,5,9,2 / 2 3 / \ 2 9 3,2,9,5 / 5 2 / \ 3 9 2,3,9,5 / 5 如何进行向下筛选呢?当堆顶的元素被取出时,我们要更新堆顶的元素来作为下一次按照优先级顺序被取出的对象,我们所需要的是将堆底部的元素放置到堆顶,然后不断地对它执行向下筛选的操作,在这个过程中,该元素和它的两个孩子节点对比,看看哪个优先级最高,如果优先级最高的是其中一个孩子,就将该元素和那个孩子进行交换,然后反复进行下去,直到无法继续交换为止,整个过程就是沿着树的高度往下爬,所以时间复杂度也是O(logk)。 ...

July 5, 2019 · 3 min · jiezi

交叉链表

无可奈何花落去 似曾相识燕归来 前言两个链表list1和list2如果中间有一个交汇点。怎样在线性时间内求出这个交汇点? 思路分别求出两个链表的长度 把长链做偏移和短链一样长 同时移动两个链表的首指针,判断节点是否相等 相等则找到交叉点 代码实现 // 定义节点 typedef struct Node { int data; struct Node *next; } Node; /// 计算链表的长度 int getListLength(Node *list) { int length = 1; while (list->next != NULL) { length++; list = list->next; } return length; } // 长链表做偏移 Node* relocateNode (Node *list, int offset) { Node *node1 = list; for (NSInteger i = 0; i < offset; i++) { node1 = node1->next; } return node1; } int findCommonNode (Node *list1, Node *list2) { // 计算两个链表的长度 int list1Length = getListLength(list1); int list2Length = getListLength(list2); // 计算两个链表的长度差值 int offset = abs(list1Length-list2Length); // 记录长链短链 Node *longList = list1; Node *shortList = list2; if (list1Length < list2Length) { longList = list2; shortList = list1; } // 长链做偏移 ,偏移后和短链一样长 longList = relocateNode(longList, offset); // 同时移动两个链表的首指针 while (shortList != NULL) { if (longList->data == shortList->data) { // 如果节点值相等 找到交叉点 return longList->data; } longList = longList->next; shortList = shortList->next; } return 0; } int main(int argc, const char * argv[]) { @autoreleasepool { Node *node1 = malloc(sizeof(Node)); node1->data = 1; Node *node2 = malloc(sizeof(Node)); node2->data = 2; Node *node3 = malloc(sizeof(Node)); node3->data = 3; Node *node4 = malloc(sizeof(Node)); node4->data = 4; Node *node5 = malloc(sizeof(Node)); node5->data = 5; Node *node6 = malloc(sizeof(Node)); node6->data = 6; Node *node7 = malloc(sizeof(Node)); node7->data = 7; node1->next = node2; node2->next = node3; node3->next = node4; node4->next = node5; node5->next = node6; node6->next = NULL; node7->next = node4; Node *list1 = node1; // 1 2 3 4 5 6 Node *list2 = node7; // 7 4 5 6 int commonValue = findCommonNode(list1, list2); NSLog(@"commonValue: %d\b", commonValue);// 4 } return 0; }测试结果 ...

July 4, 2019 · 2 min · jiezi

Leetcode137只出现一次的数字-II

题目给定一个非空整数数组,除了某个元素只出现一次以外,其余每个元素均出现了三次。找出那个只出现了一次的元素。 说明: 你的算法应该具有线性时间复杂度。 你可以不使用额外空间来实现吗? 示例 1: 输入: [2,2,3,2]输出: 3示例 2: 输入: [0,1,0,1,0,1,99]输出: 99题解根据上一道题目的经验,我们很明确的知道不能用数数字的办法去解。考虑位运算的办法,找相关的性质。 这个题其实就是求,在其他数都出现k次的数组中有一个数只出现一次,求出这个数。 而上面那个k次的是有通用解法的。 使用一个32维的数组,用这个32维的数组存储: [第31位1的总数, 第30位1的个数,…, 第1位1的个数] 假如第0位1的个数是k的倍数,那么要求的这个数在该位一定是0;若不是k的倍数,那么要求的这个数在该位一定是1。因为假如说数组中的某些数在该位置是1,那么因为这个数要么出现k次,那么出现1次。 这样我们就可以找出只出现一次那个数的二进制表示形式。二进制如何转化为10进制呢? 假如,按照上面的规则,最招找到的二进制为: A = [0, 0, 0, 0, 0, …, 1] 因为异或操作是:相同为0,不同为1。 那么久可以依次用 1, 2, 4, 8… 和依次每一位异或,即可得到最终答案。 第二部分可能不好理解,可以手动模拟下。 class Solution { public int singleNumber(int[] nums) { // 有多少个相同的数字 int N = 3; // [高位1的个数,...,低位1的个数] int[] bitNum = new int[32]; for (int i = 0; i < nums.length; i++) { int compare = 1; int num = nums[i]; for (int j = bitNum.length - 1; j >= 0; j--) { if ((compare&num) != 0) { bitNum[j]++; } compare = compare << 1; } } int compare = 1; int res = 0; for(int i = bitNum.length - 1; i >= 0; i--) { if(bitNum[i] % N != 0) { res = res ^ compare; } } return res; }}热门阅读看了很多技术书,为啥仍然写不出项目?【Spring】IOC是啥有什么好处百度社招面试题——Redis实现分布式锁【Leetcode】114. 二叉树展开为链表 ...

July 4, 2019 · 1 min · jiezi

乐字节Java变量与数据结构之二Java常量与变量

大家好,小乐又来给大家讲述Java基础知识。上次说了乐字节Java变量与数据类型之一:Java编程规范,关键字与标识符,这次将接着往下说,谈谈Java常量与变量。 1、常量1)、定义在程序执行的过程中,其值不发生改变的量。 2)、分类A:字面值常量 B:自定义常量(后面讲) 3)、字面值常量A:字符串常量 "hello" B:整数常量 12,23 C:小数常量 12.345 D:字符常量 'a','A','@' E:布尔常量 true,false F:空常量 null(后面讲) 4)、在Java中针对整数常量提供了四种表现形式A:二进制 由0,1组成。以0b开头。 B:八进制 由0,1,...7组成。以0开头。 C:十进制 由0,1,...9组成。整数默认是十进制。 D:十六进制 由0,1,...9,a,b,c,d,e,f(大小写均可)组成。以0x/0X开头。 2、变量语句:以分号来分割语句 int a;int b; 1)、定义在程序的执行过程中,其值在某个范围内可以发生改变的量。 2)、变量的定义格式:A:数据类型 变量名 = 初始化值; B:数据类型 变量名; 变量名 = 变量值; 3)、变量的两种用途:A、存值 B、取值 4)、变量的关注点:A、数据类型 B、变量名称 C、变量的值 D、变量的作用域,根据{} 来定 同一个作用域当中,变量不能重名 同一个变量在某一时刻,里面只能存在一个值,后来的值将替代/覆盖掉前面的值(前面的值永远找不到,没有撤销) 5)、使用变量注意项:A、变量本身存在 B、变量的值要存在 C、使用时一定要存在于作用域之内 其作用域:从定义开始,到离它最近的左大括号相对的右大括号结束。 6)、变量使用步骤: 注意:变量一旦被使用,那么必须保证它的值已经存在(形式参数除外) 关于Java常量与变量就讲到这里,明天我们再看Java数据类型,感谢关注乐字节! 乐字节原创,转载请注明出处!

July 3, 2019 · 1 min · jiezi

乐字节Java变量与数据结构之一Java编程规范关键字和标识符

大家好,我是乐字节的小乐,这次要给大家带来的是Java变量与数据类型。本文是第一集:Java编程规范,关键字与标识符。 一、编程规范任何地方的名字都需要见名知意; 代码适当缩进 书写过程成对编程 对代码进行合理注释 二、关键字1)、定义被Java语言赋予特定含义的单词 2)、特点全部小写 3)、注意事项A:goto和const作为保留字存在。 B:类似于Notepad++这样的高级记事本会对关键字有特殊颜色标记 4)、Java所有关键字和保留字 5)、常用关键字 三、标识符 (取的名字)1)、定义就是给类,接口,方法,变量等起名字的字符序列 2)、组成规则:A:英文大小写字母(字符统称-->中英文皆可,强烈不推荐使用中文) B:数字 C:$和_ 3)、注意事项:A:不能以数字开头 B:不能是java中的关键字 C:区分大小写 4)、常见的命名规则(见名知意)A:包 全部小写 单级包:小写 举例: com,shsxt,helloworld 多级包:小写,并用.隔开 举例:com.shsxt.helloworld 域名倒着写+模块名称 B:类或者接口 一个单词:首字母大写 举例:Student,Demo 多个单词:每个单词首字母大写 举例:HelloWorld,StudentName C:方法或者变量 一个单词:首字母小写 举例:name,main 多个单词:从第二个单词开始,每个单词首字母大写 举例:studentAge,showAllNames() D:常量 全部大写 一个单词:大写 举例:PI 多个单词:大写,并用_隔开 举例:STUDENT_MAX_AGE 命名规范:见名知意 四、注释1)、定义就是对程序进行解释说明的文字 2)、分类:A:单行注释 // B:多行注释 / 注释的内容 / C:文档注释(后面讲) /* / 3)、把HelloWorld案例写了一个带注释的版本。后面我们要写一个程序的过程。 需求: 分析: 实现: 代码体现: 4)、注释的作用A:解释说明程序,提高了代码的阅读性。 B:可以帮助我们调试程序。 后面我们会讲解一个更高端的一个调试工具 这次就到这里,后面会继续给大家带来第二集:Java常量与变量,请关注乐字节,,乐字节原创,欢迎关注,转载请注明出处

July 2, 2019 · 1 min · jiezi

你知道和你不知道的选择排序

1. 什么是选择排序?首先贴上从wiki上弄下来的关于选择排序的定义。 选择排序(Selection sort)是一种简单直观的排序算法。它的工作原理如下。首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。更加直白的解释是,每次都从数组中选出最大或者最小的元素,然后放到数组的左边。 2. 选择排序的过程展示老规矩,我们还是通过动图来看一下选择排序的过程。以下的gif来自于wiki。 然后我们再通过我制作的gif,配上数据再了解一下过程。假设我们的待排序数组还是[5, 1, 3, 7, 6, 2, 4]。 3. 选择最小值的算法我们使用Java来实现最常见的,选择最小值的选择排序,其代码如下。 private void![clipboard.png](/img/bVpLs2)![clipboard.png](/img/bVbuzbu)t(int[] arr) { int min; int minIndex; for (int i = 0; i < arr.length - 1; i++) { min = arr[i]; minIndex = -1; for (int j = i; j < arr.length; j++) { if (arr[j] < min) { min = arr[j]; minIndex = j; } } // 排序结束 交换位置 if (minIndex != -1) { exchange(arr, i, minIndex); } }}private void exchange(int arr[], int i, int j) { int temp = arr[i]; arr[i] = arr[j]; arr[j] = temp;}int[] arr = new int[]{5, 1, 3, 7, 6, 2, 4};selectionSort(arr);System.out.println(Arrays.toString(arr)); // [1, 2, 3, 4, 5, 6, 7]假设数组的长度为7,那么算法就需要进行6轮。如果数组的长度为n,则算法需要进行n - 1轮。 ...

July 2, 2019 · 2 min · jiezi

BST

江南无所谓 聊赠一枝春 前言二叉搜索树插入二叉搜索树遍历二叉搜索树高度二叉搜索树最大值什么是二叉搜索树满足条件: 左节点值 < 根节点值 < 右节点值 定义树节点 typedef struct TreeNode { int data; struct TreeNode *left; struct TreeNode *right; } TreeNode;定义树 typedef struct tree { struct TreeNode *root; } Tree;二叉搜索树的插入插入节点 void insertNode (Tree *tree, int value) { TreeNode *node1 = malloc(sizeof(TreeNode)); node1->data = value; node1->left = NULL; node1->right = NULL; if(tree->root == NULL){ tree->root = node1; } else { TreeNode *temp = tree->root; while (temp != NULL) { if(value < temp->data){ if(temp->left == NULL) { temp->left = node1; return ; } else { temp = temp->left; } } else { if(temp->right == NULL) { temp->right = node1; return; } else { temp = temp->right; } } } } }BST的前中后序遍历 // 前序遍历 根->左->右 void preorderTraverse(TreeNode *tree) { if(tree == NULL) {return;} NSLog(@"%d ", tree->data); preorderTraverse(tree->left); preorderTraverse(tree->right); } //中序遍历 左->根->右 void midTraverse(TreeNode *tree) { if(tree == NULL) {return;} midTraverse(tree->left); NSLog(@"%d ", tree->data); midTraverse(tree->right); } //后序遍历 左->右->根 void postorderTraversal(TreeNode *tree) { if(tree == NULL) {return;} postorderTraversal(tree->left); postorderTraversal(tree->right); NSLog(@"%d ", tree->data); }二叉搜索树高度 int getBSTHeight (TreeNode *node) { if (node == NULL) { return 0; } else { int leftH = getBSTHeight(node->left); int rightH = getBSTHeight(node->right); int max = leftH; if (max < rightH) { max = rightH; } return max+1; } }二叉搜索树最大值 int getMaxNum(TreeNode *node) { if (node == NULL) { return -1; } else { int leftMax = getMaxNum(node->left); int rightMax = getMaxNum(node->right); int current = node->data; int max = leftMax; if (rightMax > max) { max = rightMax; } if (current>max) { max = current; } return max; } }测试功能 int arr[] = {6,3,8,2,5,1,7}; // 创建树 Tree *tree = malloc(sizeof(Tree)); tree->root = NULL; for(int i=0; i<7; i++) { // 树中插入节点 insertNode(tree, arr[i]); } // 计算树的高度 int treeH = getBSTHeight(tree->root); NSLog(@"%d\n", treeH); // 计算树的最大值 int maxNum = getMaxNum(tree->root); NSLog(@"%d\n", maxNum);测试结果 ...

July 1, 2019 · 2 min · jiezi

程序员修仙之路优雅快速的统计千万级别uv留言送书

定义PV是page view的缩写,即页面浏览量,通常是衡量一个网络新闻频道或网站甚至一条网络新闻的主要指标。网页浏览数是评价网站流量最常用的指标之一,简称为PVUV是unique visitor的简写,是指通过互联网访问、浏览这个网页的自然人。 通过以上的概念,可以清晰的看出pv是比较好设计的,网站的每一次被访问,pv都会增加,但是uv就不一定会增加了,uv本质上记录的是按照某个标准划分的自然人,这个标准其实我们可以自己去定义,比如:可以定义同一个IP的访问者为同一个UV,这也是最常见的uv定义之一,另外还有根据cookie定义等等。无论是pv还是uv,都需要一个时间段来加以描述,平时我们所说的pv,uv数量指的都是24小时之内(一个自然日)的数据。 pv相比较uv来说,技术上比较容易一些,今天咱们就来说一说uv的统计,为什么说uv的统计相对来说比较难呢,因为uv涉及到同一个标准下的自然人的去重,尤其是一个uv千万级别的网站,设计一个好的uv统计系统也许并非想象的那么容易。 那我们就来设计一个以一个自然日为时间段的uv统计系统,一个自然人(uv)的定义为同一个来源IP(当然你也可以自定义其他标准),数据量级别假设为每日千万uv的量级。 注意:今天我们讨论的重点是获取到自然人定义的信息之后如何设计uv统计系统,并非是如何获取自然人的定义。uv系统的设计并非想象的那么简单,因为uv可能随着网站的营销策略会出现瞬间大流量,比如网站举办了一个秒杀活动。基于DB方案服务端编程有一句名言曰:没有一个表解决不了的功能,如果有那就两个表三个表。一个uv统计系统确实可以基于数据库来实现,而且也不复杂,uv统计的记录表可以类似如下(不要太纠结以下表设计是否合理): 字段类型描述IPvarchar(30)客户端来源ipDayIDint时间的简写,例如 20190629其他字段int其他字段描述当一个请求到达服务器,服务端每次需要查询一次数据库是否有当前IP和当前时间的访问记录,如果有,则说明是同一个uv,如果没有,则说明是新的uv记录,插入数据库。当然以上两步也可以写到一个sql语句中: if exists( select 1 from table where ip='ip' and dayid=dayid ) Begin return 0 Endelse Begin insert into table ....... End所有基于数据库的解决方案,在数据量大的情况下几乎都更容易出现瓶颈。面对每天千万级别的uv统计,基于数据库的这种方案也许并不是最优的。 优化方案面对每一个系统的设计,我们都应该沉下心来思考具体的业务。至于uv统计这个业务有几个特点: 每次请求都需要判断是否已经存在相同的uv记录持久化uv数据不能影响正常的业务uv数据的准确性可以忍受一定程度的误差哈希表基于数据库的方案中,在大数据量的情况下,性能的瓶颈引发原因之一就是:判断是否已经存在相同记录,所以要优化这个系统,肯定首先是要优化这个步骤。根据菜菜以前的文章,是否可以想到解决这个问题的数据结构,对,就是哈希表。哈希表根据key来查找value的时间复杂度为O(1)常数级别,可以完美的解决查找相同记录的操作瓶颈。也许在uv数据量比较小的时候,哈希表也许是个不错的选择,但是面对千万级别的uv数据量,哈希表的哈希冲突和扩容,以及哈希表占用的内存也许并不是好的选择了。假设哈希表的每个key和value 占用10字节,1千万的uv数据大约占用 100M,对于现代计算机来说,100M其实不算大,但是有没有更好的方案呢? 优化哈希表基于哈希表的方案,在千万级别数据量的情况下,只能算是勉强应付,如果是10亿的数据量呢?那有没有更好的办法搞定10亿级数据量的uv统计呢?这里抛开持久化数据,因为持久化设计到数据库的分表分库等优化策略了,咱们以后再谈。有没有更好的办法去快速判断在10亿级别的uv中某条记录是否存在呢?为了尽量缩小使用的内存,我们可以这样设计,可以预先分配bit类型的数组,数组的大小是统计的最大数据量的一个倍数,这个倍数可以自定义调整。现在假设系统的uv最大数据量为1千万,系统可以预先分配一个长度为5千万的bit数组,bit占用的内存最小,只占用一位。按照一个哈希冲突比较小的哈希函数计算每一个数据的哈希值,并设置bit数组相应哈希值位置的值为1。由于哈希函数都有冲突,有可能不同的数据会出现相同的哈希值,出现误判,但是我们可以用多个不同的哈希函数来计算同一个数据,来产生不同的哈希值,同时把这多个哈希值的数组位置都设置为1,从而大大减少了误判率,刚才新建的数组为最大数据量的一个倍数也是为了减小冲突的一种方式(容量越大,冲突越小)。当一个1千万的uv数据量级,5千万的bit数组占用内存才几十M而已,比哈希表要小很多,在10亿级别下内存占用差距将会更大。 以下为代码示例: class BloomFilter { BitArray container = null; public BloomFilter(int length) { container = new BitArray(length); } public void Set(string key) { var h1 = Hash1(key); var h2 = Hash2(key); var h3 = Hash3(key); var h4 = Hash4(key); container[h1] = true; container[h2] = true; container[h3] = true; container[h4] = true; } public bool Get(string key) { var h1 = Hash1(key); var h2 = Hash2(key); var h3 = Hash3(key); var h4 = Hash4(key); return container[h1] && container[h2] && container[h3] && container[h4]; } //模拟哈希函数1 int Hash1(string key) { int hash = 5381; int i; int count; char[] bitarray = key.ToCharArray(); count = bitarray.Length; while (count > 0) { hash += (hash << 5) + (bitarray[bitarray.Length - count]); count--; } return (hash & 0x7FFFFFFF) % container.Length; } int Hash2(string key) { int seed = 131; // 31 131 1313 13131 131313 etc.. int hash = 0; int count; char[] bitarray = (key+"key2").ToCharArray(); count = bitarray.Length; while (count > 0) { hash = hash * seed + (bitarray[bitarray.Length - count]); count--; } return (hash & 0x7FFFFFFF)% container.Length; } int Hash3(string key) { int hash = 0; int i; int count; char[] bitarray = (key + "keykey3").ToCharArray(); count = bitarray.Length; for (i = 0; i < count; i++) { if ((i & 1) == 0) { hash ^= ((hash << 7) ^ (bitarray[i]) ^ (hash >> 3)); } else { hash ^= (~((hash << 11) ^ (bitarray[i]) ^ (hash >> 5))); } count--; } return (hash & 0x7FFFFFFF) % container.Length; } int Hash4(string key) { int hash = 5381; int i; int count; char[] bitarray = (key + "keykeyke4").ToCharArray(); count = bitarray.Length; while (count > 0) { hash += (hash << 5) + (bitarray[bitarray.Length - count]); count--; } return (hash & 0x7FFFFFFF) % container.Length; } }测试程序为: ...

July 1, 2019 · 2 min · jiezi

堆排序heapsort

前言堆排序是排序算法中的一种,算法时间复杂度是O(n log(n))。这里主要介绍堆的构建以及怎样通过heapify操作完成堆排序。代码是用C语言完成的,算法不难,大家可以自己用其他语言实现一下。 什么是堆(Heap)Heap需要满足两个条件: 1.Complete Binary Tree :需要是一颗完全二叉树2.Parent > Children:父节点的值一定要大于子节点的值什么是完全二叉树 生成节点的顺序是从上往下、从左往右依次生成 如下图所示: 父节点的值大于子节点的值 如下图所示: 怎么样用代码表示堆1.假设先有这样一颗完全二叉树,它已经是一个堆了 2.1 我们按照从上往下、从左往右的顺序对每个节点数字进行编号,2.2 我们可以用一个一维数组表示 2.3 使用数组的下标来表示一个完全二叉树的好处就是从任何一个节点出发我都可以通过计算来拿到这个节点的父节点和子节点 构建堆1.假设拿到了一堆数字:10 4 3 5 1 2,这些数字放在了一颗完全二叉树上面,如下图所示: 2.heapify: 把完全二叉树调整成堆,我们把这种操作起个名字叫:heapify 1.第一次heapify操作:把4(父节点)、10、3这三个子节点进行比较,找到最大值和父节点进行交换交换后如下图: 2.第二次heapify操作,把4(父节点)、5、1这三个子节点进行比较,找到最大值和父节点进行交换交换后如下图: 3.这样我们就生成了一个堆:满足完全二叉树、父节点值大于子节点的值 123步的代码实现: void swap(int *tree, int max, int i) { int temp = tree[i]; tree[i] = tree[max]; tree[max] = temp; } /** 对一个二叉树进行heapify操作 @param tree 表示二叉树的数组 @param n 二叉树的节点个数 @param i 表示要对哪个节点进行heapify操作 */ void heapify(int *tree, int n, int i) { if (i >= n) { // 递归函数出口 return; } // 找到i节点的两个子节点 int c1 = i*2 + 1; int c2 = i*2 + 2; // 找个三个节点的最大值 假设i是最大值 int max = i; if(c1 < n && tree[c1] > tree[max]) { // c1 < n 表示节点下面没有子节点 max = c1; } if (c2 < n && tree[c2] > tree[max]) { max = c2; } if (max != i) { // max != i b如果i已经是最大值了就不用交换了 swap(tree, max, i); heapify(tree, n, max);//max节点继续heapify } } int main(int argc, const char * argv[]) { @autoreleasepool { int tree[] = {4,10,3,5,1,2}; int n = 6; heapify(tree, n, 0); for (int i = 0; i < n; i++) { printf("%d\n", tree[i]); } } return 0; }输出结果: ...

July 1, 2019 · 2 min · jiezi

Leetcode135分发糖果

# 题目 老师想给孩子们分发糖果,有 N 个孩子站成了一条直线,老师会根据每个孩子的表现,预先给他们评分。 你需要按照以下要求,帮助老师给这些孩子分发糖果: 每个孩子至少分配到 1 个糖果。相邻的孩子中,评分高的孩子必须获得更多的糖果。那么这样下来,老师至少需要准备多少颗糖果呢? 示例 1: 输入: [1,0,2]输出: 5解释: 你可以分别给这三个孩子分发 2、1、2 颗糖果。示例 2: 输入: [1,2,2]输出: 4解释: 你可以分别给这三个孩子分发 1、2、1 颗糖果。 第三个孩子只得到 1 颗糖果,这已满足上述两个条件。题解我们首先给每一个小朋友都发糖果,保证每位喜至少分配到一个糖果;从左到右遍历,考虑右边的小朋友比左边小朋友排名高的情况,此时更新为 candy[i] = candy[i - 1] + 1,暂时不考虑左边比右边高的情况;从右到左遍历,考虑左边比右边高的情况,希望左边比右边高,但是又需要考虑不破坏第二点中的已经满足的规则,所以更新条件为candy[i] = max(candy[i], candy[i + 1] + 1);把所有的分配加起来;class Solution { public int candy(int[] ratings) { if (ratings == null || ratings.length == 0) return 0; int[] resCandy = new int[ratings.length]; for (int i = 0; i < resCandy.length; i++) { resCandy[i] = 1; } for (int i = 1; i < ratings.length; i++) { // 比左边的小朋友排名更高. if (ratings[i] > ratings[i - 1]) { resCandy[i] = resCandy[i - 1] + 1; } } for (int i = ratings.length - 2; i >= 0; i--) { // 当前比右边的小朋友排名更高.resCandy[i] = resCandy[i + 1] + 1 // 同时需要保证不破坏当前比左边小朋友高的规则 if (ratings[i] > ratings[i + 1]) { resCandy[i] = Math.max(resCandy[i + 1] + 1, resCandy[i]); } } int res = 0; for (int i = 0; i < resCandy.length; i++) { res += resCandy[i]; } return res; }}热门阅读看了很多技术书,为啥仍然写不出项目?【Spring】IOC是啥有什么好处百度社招面试题——Redis实现分布式锁【Leetcode】114. 二叉树展开为链表 ...

July 1, 2019 · 1 min · jiezi