数据结构是相互之间存在一种或多种关系的数据的集合。java
分为线性结构和非线性结构,node
即物理结构,分四种(存储数据时不仅要存储元素的值,还要存储数据元素间的关系)面试
时间复杂度就是程序逻辑执行的次数。一般在求解时间复杂度的时候,会对其进行简化。下面是推导大 O 阶的方法:算法
常见的时间复杂度:数组
int num = 0, n = 100; num = (1 + n) * n / 2; printf(num);
主义上面的时间复杂度是常数阶 O(1),而不是 O(3).数据结构
for(int i=0; i<n; i++) { // 执行时间复杂度为O(1)的操做 }
上面的时间复杂度是 O(n).架构
int cnt = 1; while (cnt < n) { cnt *= 2; // 执行时间复杂度为O(1)的操做 }
上面的时间复杂度是 O(logn).ide
int i, j; for (int i=0;i<n;i++) } for (int j=0;j<n;j++) { // 执行时间复杂度为O(1)的操做 } }
上面的计算的时间复杂度是 O(n2)函数
int i, j; for (int i=0;i<n;i++) } for (int j=0;j<m;j++) { // 执行时间复杂度为O(1)的操做 } }
上面的计算的时间复杂度是 O(nm)学习
下面的程序的时间复杂度也是 O(n2):
int i, j; for (int i=0;i<n;i++) } for (int j=i;j<n;j++) { // 执行时间复杂度为O(1)的操做 } }
执行的次数,O(f(n)),其中 f(n) 是执行的次数,表示执行时间与 f(n) 成正比
时间复杂度的大小关系:
O(1)<O(log2n)<O(n)<O(nLogn)<O(n^2)<O(n^3)<O(2^n)<O(n!)<O(nn)
所需的物理存储空间
题目:算法 void fun(int n){ int i=1; while(i<=n) { i*=2; } } 的时间复杂度是? 思路:函数中运算次数最多的是 i*=2; 这一行,那么假设它执行了 t 次,t 次时 i=2^t。 所以,有:2^t<=n,因而得 t<=log2n ,因而可得 O(log2n)
线性表是具备相同数据类型的n个数据元素的有限序列。
线性表的顺序存储,即存储在一组连续的存储单元里,如数组。顺序表能够是静态分配的,也能够是动态分配的。动态分配的,好比用指针指示动态数组,静态分配的是建立数组的时候就指定数组的大小。
注意,在线性表当中插入或者删除数据是要移动其余元素的,而访问的是否直接使用索引访问便可。因此,对于线性表访问第i个位置的元素的时间复杂度为 O(1),在第i个位置插入元素的时间复杂度为 O(n).
普通的链表,定义的形式是
public class LinkedList<E> { transient int size = 0; transient Node<E> first; private static class Node<E> { E item; Node<E> next; Node(E element, Node<E> next) { this.item = element; this.next = next; } } }
这里使用泛型的来表明链表的每一个节点中存储的数据,使用内部类 Node 类定义链表的一个节点。
下面的是一份基于Java的双向链表的实现:
public class LinkedList<E> { transient int size = 0; transient Node<E> first; transient Node<E> last; private static class Node<E> { E item; Node<E> next; Node<E> prev; Node(Node<E> prev, E element, Node<E> next) { this.item = element; this.next = next; this.prev = prev; } } }
借助数组类描述链表,结点有数据域 data 和指针域 next,描述(这种设计的好处是适用于不支持指针的计算机语言)
栈是一种后进先出的数据结构,下面是一种使用单向链表实现的栈:
public class Stack<E>{ private int size; private Node<E> first; private static class Node<E> { E element; Node<E> next; Node(E element, Node<E> next) { this.element = element; this.next = next; } } public void push(E element) { first = new Node<E>(element, first); size++; } public E pop() { if (size == 0) { throw new UnsupportedOperationException("The stack is empty."); } size --; Node<E> oldFirst = first; first = oldFirst.next; return oldFirst.element; } public boolean isEmpty() { return size == 0; } }
题1:3个不一样的元素依次进栈,能获得几种不一样的出栈序列?
5种,abc acb bac bca cba,最后一种若是以c开头,那么ab必然已经存入了栈中,取出的顺序只能是ba,即以c开头的只有cba.
题2:a b c d e f 以所给的顺序依次进栈,若在操做时容许出栈,这得不到的序列为?
A fedbca B bcafed C dcefba D cabdef
这种题应该从每一个选项的第一个字母入手,以C为例,若是d处在第一个,那么说明前面的a b c确定已经存在栈中,那么它们必然按照c b a的顺序出来;
若是题目中的c b a的出现次序不对,那么就得不到。而后再使用相同的思路判断第二个字符的状况。答案D
队列也是一种线性表,特性是先入先出,队列和栈的主要区别是插入、删除操做的限定不同。下面是基于 Java 的一种使用链表来实现的队列:
public class Queue<E> { private Node<E> first, last; private int size; private static class Node<E> { E element; Node<E> next; Node(E element, Node<E> next) { this.element = element; this.next = next; } } public void enqueue(E element) { size++; Node<E> node = new Node<E>(element, null); if (last == null) { first = node; last = node; return; } last.next = node; last = node; } public E dequeue() { if (size == 0) { throw new UnsupportedOperationException("The queue is empty."); } size--; E element = first.element; first = first.next; return element; } public boolean isEmpty() { return size == 0; } }
队首和队尾都容许入队和出队的队列。
背包是一种不支持从中删除元素的集合数据类型,它的目的就是帮助用例收集并迭代遍历全部收集到的元素。使用背包的不少场景可能使用栈或者队列也能实现,可是使用背包能够说明元素存储的顺序不重要。下面的是一份基于 Java 的背包的实现,在这里咱们只是在以前栈的代码的基础之上作了一些修改,并让其实现 Iterable 接口以在 foreach 循环中使用背包遍历元素:
public class Bag<E> implements Iterable<E>{ private int size; private Node<E> first; private static class Node<E> { E element; Node<E> next; Node(E element, Node<E> next) { this.element = element; this.next = next; } } public void add(E element) { first = new Node<E>(element, first); size++; } public Iterator<E> iterator() { return new ListIterator(); } private class ListIterator implements Iterator<E> { private Node<E> current = first; @Override public boolean hasNext() { return current != null; } @Override public E next() { E element = current.element; current = current.next; return element; } @Override public void remove() {} } public boolean isEmpty() { return size == 0; } }
题:一棵有n个结点的树,全部结点的度数之和为_______.
问题转换成:一棵有3个结点的树,全部结点的度数之和为____. 由于题目是选择,因此应该尽可能简化题目。答案n-1
image
在上图中左侧的是彻底二叉树,右侧的是满二叉树。
说明:所谓的编号就是指每层从左到右,按照满二叉树的形式编号,上面的满二叉树每一个结点的值就是它们的编号。这个编号也是咱们在使用顺序存储的时候对应数组的下标。
1.顺序存储
这种存储方式,能够理解为使用数组存储,注意开始存储的下标是1,这是为了与二叉树的性质对应,另外有时候咱们也能够将数组的第一个元素做为哨兵。顺序存储的基本思想是,按照满二叉树的编号顺序,若是指定编号(其实就是数组的下标)处有结点的话,数组指定位置的值即为结点的值,不然为0(表示空结点)。固然,也能够在各个元素中存放一些具备具体含义的值。
如图所示的树在数组中的实际存储为:- 1 2 3 0 4 0 5 0 0 6 0(第0位不使用)。
2.链式存储
下面是使用Java代码实现的一个二叉树,这里每一个结点要包含左右两个子结点以及相应的数据实体。显然,
public class Tree<E> { private Node<E> root; private static class Node<E> { E element; Node<E> leftChild; Node<E> rightChild; Node(Node<E> leftChild, E element, Node<E> rightChild) { this.leftChild = leftChild; this.element = element; this.rightChild = rightChild; } } }
说明:上述三幅图从左到右的遍历方式依次是先序遍历、中序遍历和后序遍历。
做为练习,这里给出遍历二叉树的方法,能够观察一下二叉树的实现,以及中序遍历的时候输出的二叉树的值,其中 outNode() 方法用来输出结点的值:
/* 先序遍历 */ public void former() { former(root); } private void former(Node<Key, Value> node) { if (node == null) return; outNode(node); // 先序 former(node.leftChild); former(node.rightChild); } /* 中序遍历,注意中序输出的结果和排序的结果的关系 */ public void center() { center(root); } private void center(Node<Key, Value> node) { if (node == null) return; center(node.leftChild); outNode(node); // 中序 center(node.rightChild); } /* 后序遍历 */ public void latter() { latter(root); } private void latter(Node<Key, Value> node) { if (node == null) return; latter(node.leftChild); latter(node.rightChild); outNode(node); // 后序 }
结论:
注意若是只知道二叉树的先序序列和后序序列是没法惟一地肯定一棵二叉树的。
image
如图所示的二叉树,它的三种遍历方式:
说明:先序遍历的时候,根结点处在第1的位置;中序遍历的时候根节点前面是左子树,后面是右子树;后序遍历的时候,根结点处在最后的位置。能够根据上面的思路,依次进行判断,从而能够写出完整的树。
使用一个V*V矩阵来表示,其中V是顶点的个数。矩阵的某个元素中存储的能够是布尔变量,表示该边是否存在,在实际应用中也能够将其设置为某个边的权重。使用这种方式的缺点是当数据量比较大的时候会占用很大的存储空间。
即便用一个以顶点为数组的索引的列表数组来表示图。如下是图的一个基于Java的实现:
public class Graph { private Bag<Integer>[] adj; public Graph(int V) { adj = new Bag[V]; for (int i=0;i<V;i++) { adj[i] = new Bag<Integer>(); } } }
能够看出实际在这里咱们使用了背包来存储图的数据。adj 的具体含义是:好比,若是顶点 0 对应的数组元素是 adj[0],这里的 adj[0] 是一个背包,它里面存储了与顶点 0 相连的其余顶点。若是是无向图的话,只要在背包中存储全部相关的顶点就能够了。若是是有向图的话,须要存储该点指向的顶点的值。
它跟邻接表的区别在于,在邻接表中,同一条边由用两个顶点表示,而在邻接多重表中只用一个顶点表示。
上图就是边集数组的表示方法,能够看出它使用一个数组来存储各个边。