数据结构与算法
1. 集合:所有元素同属于一个集合,除此之外数据元素之间灭有任何关系2. 线性表:数据元素之间存在一一对应的关系(简单来说,线性表中的元素都有一个序的概念)。栈和队列是操作受限的线性表3. 树:一对多4. 图:多对多数据结构的物理表示:顺序映象(连续存储)和非顺序映象(离散存储)
线性表的顺序映象实现
编程注意事项:
add(String element, int index)
1. 检查index在线性表中的位序是否合法([0,size])
2. 检查,数组容量(不够的话扩容(1.5), 扩容之后可能容量可能超出线性表的容量上限)。//移位运算的优先级,
3. 根据指定位序,放入元素(放入元素之前, 移动元素,空出带插入位置,放入) //System.arrayCopy()
4. Size++
remove(index)
1. 检查index位序是否合法([0,size - 1])
2. 移动位置,覆盖(删除)
3. size--
两个使用的api: Arrays.copyOf(), System.arrayCopy() //copy以后记得将数组赋值给原数组
isEmpty()
size()
toString //使用更高效的StringBuilder类
indexOf(Stirng e) //处理当查找空元素,以及查找的元素有null。(==和equals的使用)
例:
public class SequentialLinearList {
private String[] elements; //实际盛放数据的数组
private int size; //数组的装数据的长度
private final static int MAX_ARRAY_SIZE = Integer.MAX_VALUE; //定义一个线性表的容量上线
private final static int DEFAULT_INIT_ELEMENTS = 10; //默认初始化elements的数据元素个数10个
public SequentialLinearList() { //构造方法执行初始化动作
elements = new String[DEFAULT_INIT_ELEMENTS]; //避免硬编码
}
public SequentialLinearList(int capacity) {
if(capacity <= 0 || capacity > MAX_ARRAY_SIZE) {
throw new IllegalArgumentException("illegal arguments " + capacity);
}
elements = new String[capacity];
}
public void add(String e,int index) { //添加元素
rangeCheckForAdd(index); //校验线性表中待插入元素的指定位置,是否合法
ensureCapacityInternal(size + 1); //首先判断一下,当前的数组中,是否可以在插入元素
int copyLength = size - index; //将元素,放入线性表中,int copyLength = (size - 1) - index + 1;先将size都换到0开始
System.arraycopy(elements,index, elements,index + 1, copyLength); //api比较智能 复制的范围(index,index + size - index -1)
elements[index] = e; //components at positions <code>srcPos</code> through
size++; //<code>srcPos+length-1</code> were first copied to a temporary
}
private void ensureCapacityInternal(int minCapacity) { //所需的最小容量都无法满足要求
if(minCapacity - elements.length >0 ) { //minCapacity > elements.length与minCapacity-elements.length>0不等价。
//当size取Integer.MAX_VALUE时minCapacity<0,前面不成立,后面成立
int oldCapacity = elements.length; //访问局部变量要比访问成员变量,要快
int newCapacity = oldCapacity + (oldCapacity >> 1); //扩容,一次扩多少呢?效率和内存利用率的一个折中,扩容多少呢?(1.5倍扩容),要加括号,优先级
//如果一次可以添加多个元素的话,判断扩容之后,容量是否足够(我们一个一个的添加元素不会遇到这种情况)
newCapacity = hugeCapacity(newCapacity); //临界问题,扩容的数值超出数组最大容量
elements = Arrays.copyOf(elements, newCapacity); //记得赋值给原数组
}
}
private int hugeCapacity(int newCapacity) {
if(newCapacity < 0) {
if(size + 1 < 0) { //可能,线性表中存放的元素个数还未达到上限,但是,扩容之后的数值,超出了线性表中的元素个数上限
throw new OutOfMemoryError();
}
newCapacity = MAX_ARRAY_SIZE;
}
return newCapacity;
}
private void rangeCheckForAdd(int index) {
if(index < 0 || index > size) {
throw new ArrayIndexOutOfBoundsException(outOfBoundsMsg(index));
}
}
/*
private void arrayCopy (String[] src, int offset, String[] desc, int length) { //arrayCopy自己写的写法
for (int i = offset,j = 0; i < offset + length; i++,j++) {
desc[j] = src[i];
}
}*/
public String remove(int index) { //删除元素
rangeCheckForRemove(index); //检测删除元素位置是否合法
int movNum = size - index - 1; //删除元素,int movNum = (size - 1) - index + 1 - 1;
String oldValue = elements[index];
if(movNum > 0) {
System.arraycopy(elements,index + 1, elements, index, movNum);
}
elements[size - 1] = null;
size--;
return oldValue;
}
public void add(String e) {
ensureCapacityInternal(size + 1); //容量检测
elements[size] = e;
size++;
}
private void rangeCheckForRemove(int index) {
if(index < 0 || index >= size) {
throw new ArrayIndexOutOfBoundsException(outOfBoundsMsg(index));
}
}
private String outOfBoundsMsg(int index) { //降低代码的耦合
return "index outOfBounds , size = " + size + ", index = " + index;
}
@Override
public String toString() {
if(size == 0) {
return "[]";
}
StringBuilder builder = new StringBuilder("["); //使用StringBuilder提高效率
for (int i = 0; i < size; i++) {
builder.append(elements[i]);
if(i == size - 1) {
builder.append(']');
break;
}
builder.append(", "); //方法返回的是 StringBuilder,可以链式调用
}
return builder.toString();
}
public int size() {
return size;
}
public boolean isEmpty() {
return size == 0;
}
public int indexOf(String e) { //查找当前线性表中,和给定元素相匹配的,第一个元素出现的位置
if(null == e) { //当查找的元素为null时
for(int i = 0; i < size; i++) {
if(e == elements[i]) { //用==判断
return i;
}
}
} else {
for(int i = 0; i < size; i++) {
if(e.equals(elements[i])) { //肯定了e不等于null,故用它的equals方法
return i;
}
}
}
return -1;
}
}
线性表(顺序映象)编程中的知识点
1. 扩容问题,数组超过预设的容量,如何考虑效率和内存的折中,sun公司做的实验是1.5容量,
容量上限是Integer.MAX_VALUE,预设容量是10
2. 计算临界容量时候要考虑,线性表中存放的元素个数还未达到上限,但是,扩容之后的数值,超出了线性表中的元素个数上限
3. 将检查用户输入单独抽出一个方法,输入不合法则抛出异常。注意插入和删除的合法输入差一个
4. 容量改变后记得size++或size--
5. 查找线性表元素时,针对用户输入为null时的搜索。以及用户输入不为null搜索(==和equals的使用)
//和源码比较发现在使用ArryList的无参构造法创建数组时,系统默认创建的是一个空数组{},该数组经过.length为0
//防止创建了数组对象,却一直不使用它,浪费空间,直到向里面添加元素时才扩容为默认长度10
//这个数组只是起一个标志作用,int[] a={} a.length=0, 数组在java中是一个内置对象,a中此时存的数组对象的地址
Arrays.copyOf()和System.arrayCopy()
1. elements = Arrays.copyOf(elements, newCapacity);
//将elements[]数组复制到newCapacity长度的数组中
2. System.arrayCopy(Object src, int srcPos, Object dest, int destPos, int length)
//当src数组和dest数组是同一个时,函数会通过一个temp数组,将原数组中的srcPos到xx长度为length
//复制到原数组的destPos到xx长度为length
StringBuilder类
普通String对象一旦创建,其值无法被修改(见字符串内存图解),效率很低
StringBuilder直接操作字节流,效率更高。
使用:
StringBuilder builder = new StringBuilder("[")
builder.append(']');
builder.toString();
访问局部变量要比访问成员变量快
程序中经常使用elements.length,可以将这个值提取出来
例如:
int oldCapacity = elements.length;
数学上的等价在计算机中不等价
minCapacity > elements.length 与 minCapacity-elements.length > 0 不等价。
minCapacity = Integer.MAX_VALUE + 1 ,elements.length = Integer.MAX_VALUE
第一个式子小于0的数大于Integer.MAX_VALUE不成立
第二个式子成立
搜索元素考虑null的问题
if(null == e) { //当查找的元素为null
for(int i = 0; i < size; i++) {
if(e == elements[i]) { //null用==判断
return i;
}
}
} else {
for(int i = 0; i < size; i++) {
if(e.equals(elements[i])) { //肯定了e不等于null,故用e的equals方法
return i;
}
}
}
内部类的使用static避免内存泄漏 (见详图static内部类)

普通成员内部类对象都隐式的持有其外部类对象的一个引用
(只要inner类对象有被使用,inner有指向outer类对象的隐式引用,outer类对象无法被销毁)
但是一旦该内部类加了static修饰符,该内部类对象不会再持有其外部类对象的引用,可以规避内存泄露
注:
普通类对象中包含一个对其它对象的引用,若没有引用变量指向普通类对象,普通类对象会变成垃圾(不同之处是指向的方向)
线性表的非顺序映象实现 (链表)
public class MyLinkedList {
private int size; //链表长度
private Node first; //指向第一个节点
private Node last; //指向最后一个节点
private static class Node { //链表节点,内部类使用static避免内存泄漏
String data;
Node next;
}
public void insertHead(String e){ //头插法
linkNode(null,e);
}
public void insertTail(String e){ //尾插法
linkNode(last,e);
}
public void add(String e, int index) { //添加节点
checkAddInput(index); //检查输入是否合法
Node pre = getPreNode(index); //获取插入节点的前一个节点
linkNode(pre, e); //链接操作
}
private void linkNode(Node pre, String e) {
Node node = new Node();
node.data = e;
if(size == 0){ //如果里面没有元素
node.next =first;
first = node;
last = node;
}else {
if(pre == null){ //如果插在0处
node.next =first;
first = node; //错误核查如果按错误执行下去为什么会一直运行
}else {
if(pre.next == null){ //如果插在尾部改last
last = node;
}
node.next = pre.next; //添加元素
pre.next = node;
}
}
size++;
}
private Node getPreNode(int index) {
if(index == 0 || first == null){
return null;
}
//另法获取PreNode更简洁
/*Node node = first;
for(int i = 1; i < index; i++) { //i的作用类似于count
node = node.next;
}
return node;*/
int count = 1;
Node node = first;
while(node!= null){
if(count == index){
return node;
}
count++;
node= node.next;
}
return node;
}
private void checkAddInput(int index) {
if (index < 0 || index > size) {
throw new IllegalArgumentException(outOfBoundMsg(index));
}
}
private String outOfBoundMsg(int index) {
return "用户输入非法 index = " + index + ",size = " + size;
}
public String remove(int index) { //删除节点
checkRemoveInput(index); //检查输入
Node pre = getPreNode(index); //获取删除节点的前一个节点
String str = unlinkNode(pre);
return str;
}
private String unlinkNode(Node pre) {
String str = first.data ; //删除的节点
if(size==1){ //只有一个节点
first=last=null;
}else {
if(pre == null){ //删除第一个节点
first = first.next;
}else {
if(pre.next == null) //删除最后一个节点
{
last = pre;
}
str = pre.data;
pre.next = pre.next.next;
}
}
size--;
return str;
}
public int indexOf(String e){ //获取字符所在位置
int index = 0;
if(e == null){
for ( Node node = first; node!=null; node = node.next ) {
if(node.data == e){ //使用==比较,null用==比较
return index;
}
index++;
}
}else {
for ( Node node = first; node!=null; node = node.next ) {
if(node.data.equals(e)){ //使用equals比较,而且还要考虑使用的元素不能为空,空则用==
return index;
}
index++;
}
}
return -1;
}
private void checkRemoveInput(int index) {
if(index < 0 || index > size-1 || size == 0){ //注意长度为0的情况
throw new IllegalArgumentException(outOfBoundMsg(index));
}
}
public boolean isEmpty(){
if(size == 0){
return true;
}
return false;
}
public int getSize(){
return size;
}
@Override
public String toString(){
if(first==null){
return "[]";
}
StringBuilder stringBuilder= new StringBuilder("["); //使用stringBuilder效率更高
for (Node node = first; node!=null; node = node.next) {
stringBuilder.append(node.data);
if(node.next==null){
break;
}
stringBuilder.append(", ");
}
stringBuilder.append(']');
return stringBuilder.toString();
/*if(first==null){ //另一种输出效率低
return "[]";
}
String str="[";
for (Node node = first; node!=null; node = node.next) {
str+=node.data;
if(node.next==null){
break;
}
str+=", ";
}
str+=']';
return str;*/
}
}
单链表编程中的知识点:
1. 添加节点,里面没有元素插入,插入在头部,插入在尾部,三种情况分别考虑
2. 删除节点,只有一个节点删除,删除第一个节点,删除最后一个节点,三种情况分别考虑
3. 查找线性表元素时,针对用户输入为null时的搜索。以及用户输入不为null搜索(==和equals的使用)
栈(顺序映象)实现
顺序栈实现:
public class MyStack {
private int top; //栈顶指针
private String[] elements; //数组
private final static int MAX_CAPACITY = Integer.MAX_VALUE; //最大长度
private final static int DEFAULT_INIT_ELEMENTS = 10; //初始长度
public MyStack() {
elements = new String[DEFAULT_INIT_ELEMENTS];
}
public void push(String e) { //入栈
ensureCapacity(top+1); //确保容量足够,top >= elements.length-1 ,先化成都由0开始
elements[top] = e; //入栈
top++;
}
private void ensureCapacity(int mincapacity ) {
if (mincapacity - elements.length >= 0) {
int oldCapacity = elements.length;
int newCapacity = oldCapacity +(oldCapacity>>1);
newCapacity = checkNewCapacity(newCapacity);
elements = Arrays.copyOf(elements,newCapacity );
}
}
private int checkNewCapacity(int newCapacity) {
if(newCapacity<0){
if(top+2 < 0 ){
throw new OutOfMemoryError("内存溢出");
}
newCapacity = MAX_CAPACITY;
}
return newCapacity;
}
public String pop(){ //出栈
checkEmpty();
top--;
return elements[top];
}
private void checkEmpty() { //为了抛异常
if(isEmpty()){
throw new IllegalArgumentException("栈为空");
}
}
public String peek(){ //输出栈顶
checkEmpty();
return elements[top-1];
}
@Override
public String toString(){ //tostring
if(top==0){
return "[]";
}
StringBuilder stringBuilder = new StringBuilder("[");
for (int i = top-1; i >= 0 ; i--) {
stringBuilder.append(elements[i]);
if(i == 0){
stringBuilder.append("]");
break;
}
stringBuilder.append(',');
}
return stringBuilder.toString();
}
public boolean isEmpty(){ //是否为空
if(top==0){
return true;
}
return false;
}
}
注意:
1. 注意事项和顺序表数组实现类似,需要考虑扩容
2. tostring方法访问为null情形
3. peek()方法输出栈顶元素,不删除元素
4. 栈顶top指向的是承装元素的下一个空间
队列的实现(顺序映象) (见详图顺序映象队列空和队列满)

解决队空和队满判断条件相同的方案:(rear=front)
1. 添加一个标志位,来区分当前究竟是队空还是队满的状态。
2. 浪费一个空间,约定以队列头指针在队尾的下一位置(指环状的下一位置)上,作为队列满的标志。
//front指向队头,rear指向队尾的下一个元素
public class MyLinearQueue {
private String[] elemets ;
private final static int MAX_CAPACITY = Integer.MAX_VALUE; //最大容量
private final static int ORIGIN_CAPACITY = 10; //默认容量
private int front; //队头
private int rear; //队尾
private int size; //队列长度
public int getSize(){
return (rear-front + elemets.length)%elemets.length;
}
public MyLinearQueue(){
elemets = new String[ORIGIN_CAPACITY];
}
public void enQueue(String e){ //入队
size = getSize();
ensureCapacity(size + 2 ); //确保容量足够,注意这里的最小容量是size + 2,循环队列要留一个空间浪费
elemets[rear] = e; //进队
rear = next(rear);
}
public String poll(){ //获取队头元素
checkIsEmpty();
return elemets[front];
}
public String deQueue(){ //出队
String str = poll();
front = next(front);
return str;
}
private void checkIsEmpty() { //检查是否为空
if(isEmpty()){
throw new ArrayIndexOutOfBoundsException("队列为空");
}
}
private void ensureCapacity(int minCapacity) {
if( minCapacity- elemets.length>0){
int oldCapacity = elemets.length;
int newCapacity = oldCapacity + (oldCapacity>>1);
newCapacity = checkNewCapacity(newCapacity);
String[] temp = new String[newCapacity];
elemets = arrryCopy(elemets,front,temp,getSize()); //自己写数组的拷贝操作
front = 0;
rear = size ;
}
}
private String[] arrryCopy(String[] elemets, int front, String[] temp, int size) {
for( int i=0; size > 0 ; size--, front = next(front),i++){ //i++忘了
temp[i] = elemets[front];
}
return temp;
}
private int next(int cur){
return (cur+1)%elemets.length;
}
private int checkNewCapacity(int newCapacity) {
if(newCapacity<0){
if(size+2<0){
throw new OutOfMemoryError("内存溢出");
}
newCapacity = MAX_CAPACITY;
}
return newCapacity;
}
public boolean isEmpty() { //是否为空
if (getSize() == 0) {
return true;
}
return false;
}
@Override
public String toString (){ //toString
if(isEmpty()){
return "[]";
}
StringBuilder stringBuilder = new StringBuilder("[");
for (int i = front ; rear != i ; i = next(i)) {
stringBuilder.append(elemets[i]+" ");
}
stringBuilder.append("]");
return stringBuilder.toString();
}
}
注意:
1. 队头出队,队尾进队
2. 出队操作(front+1)%elements.length,入队操作(rear+1)%elements.length,队列空间(rear-front + elemets.length)%elemets.length
3. 扩容操作还是1.5 容量,其中判断的最小容量是size + 2,循环队列要留一个空间浪费(此时front指向队头,rear指向队尾的下一个元素)
4. poll()是获取队头元素但不删除元素
5. 大量使用private int next(int cur){return (cur+1)%elemets.length;} //都是加一数组长度取余,抽成一个方法,
6. 队空判断条件front==rear,队满的条件(rear+1)%elements.length==front
树
树的定义
树(Tree)是n( n>=0)个结点的有限集合。在任意一颗非空树中:
1. 有且仅有一个特定的成为根(Root)的结点的
2. 当n>1的时,其余结点可分文m( m>0 )个互不相交的有限集T1,T2,..., Tm,
其中每一个集合本身又是一棵树,并且称之为根的子树。
注:树的定义本身是一个递归定义,即在树的定义中又用到树的概念。
基本术语
1. 树的结点:包含一个数据元素即若干指向其子树的分支。结点拥有的子树的数目,称之为结点的度(degree)。
2. 度为0的结点称为叶子,或终端结点。度不为0的结点称为非终端结点或分支节点
3. 节点的子树的根,称为该结点的孩子(child),相应的该结点称为孩子的双亲(parent)
4. 同一个双亲的孩子之间互称兄弟(sibling)
5. 结点的祖先是从根到该结点所经分支上所有的结点。
6. 以某结点为根的子树中的任意一结点都称为该结点的子孙
7. 结点的层次(Level)从根开始定义,根为第一层,根的孩子为第二层。若某结点为在第l层,则其子树的根就在l+1层
8. 树中结点的最大层次称为树的深度(depth)或高度
二叉树
二叉树是另一种树形结构,它的特点是每个结点至多只有两颗子树(即二叉树中不存在度大于2的结点),
并且,二叉树有左右之分,其次序不能任意颠倒(有序)。有五种形态空树、只有根节点、只有左子树、只有右子树,左右子树都有
二叉树具有以下重要特性
1. 二叉树,在第i层至多有2^(i-1)个结点 (主要是在理论上使用的比较多)
2. 深度为k的二叉树至多有(2^k)-1个结点
3. 对任何一颗二叉树T,如果其终端结点数位n0,度为2的结点数位n2,则n0=n2+1 //1+n1+2*n2 = no+n1+n2
4. 具有n个结点的完全二叉树,的深度为log2n + 1(向下取整)
5. 如果对一颗有n个结点的完全二叉树(其深度为log2n + 1)的结点按层序编号,则对任意一结点,有
1)如果i=1,则结点i是二叉树的根,无双亲;如果i > 1,则其双亲parent(i)是节点i/2
2)如果2i > n,则结点i无左孩子(结点i为叶子结点);否则其左孩子lchild(i)是节点2i
3)如果2i+1 > n,则结点i无右孩子,否则,其右孩子rchild(i)是结点2i+1
中序后序遍历构造二叉树,深度优先遍历,和广度优先遍历 (见后详图中序后序创建二叉树及中序先序创建二叉树)


public class BinaryTree {
//树节点定义
public static class Node {
char data;
Node lChild;
Node rChild;
public Node() {
}
public Node(char data, Node lChild, Node rChild) {
this.data = data;
this.lChild = lChild;
this.rChild = rChild;
}
}
//递归创建树的节点
public Node build(char[] mid ,int lmid,int rmid ,char[] post ,int lpost,int rpost){
//取出根节点的值
char rootData = post[rpost];
//获取中序遍历中根节点的位置
int midRootIndex = getMidRootIndex(mid, lmid, rmid, post[rpost]);
//检查输入是否合法
checkInput(midRootIndex);
//左右子树的长度
int lNum = midRootIndex - lmid;
int rNum = rmid - midRootIndex;
//创建左子树
Node l,r;
if(lNum == 0){
l = null;
}else {
l = build(mid,lmid,midRootIndex-1,post,lpost,lpost+lNum-1); //最主要的错误就是这范围选择
}
//创建右子树
if(rNum == 0){
r = null;
}else {
r = build(mid,midRootIndex+1,rmid,post,rpost-rNum, rpost - 1);
}
//创建根节点
return new Node(rootData,l,r); //最后返回的是树的根节点
}
private void checkInput(int midRootIndex) {
if(midRootIndex == -1)
{
throw new IllegalArgumentException("输入非法");
}
}
private int getMidRootIndex(char[] mid, int lmid, int rmid, char root) {
for (int i = lmid; i <= rmid ; i++) {
if(mid[i] == root){
return i;
}
}
return -1;
}
//先序遍历
public static void preOrder(Node root){
if(root != null){
//访问根节点
System.out.print(root.data+" ");
//访问左子树
preOrder(root.lChild);
//访问右子树
preOrder(root.rChild);
}
}
//层次遍历
public static void BSF(Node root){
//创建队列
Queue<Node> queue = new LinkedList<>();
queue.add(root);
while(!queue.isEmpty()){ //这里是一个循环
//出队
Node node = queue.remove();
System.out.print( node.data+" " );
//左孩子进队
if(node.lChild!=null){
queue.add(node.lChild);
}
//右孩子进队
if(node.rChild!=null){
queue.add(node.rChild);
}
}
}
}
注意:
层次遍历算法:(借助队列,先进先出)
1. 首先根节点出队,访问根节点,然后判断是否有左孩子,如果有左孩子,入队
2. 继续判断,该节点是否有右孩子,有则入队
3. 重复1,2 直到队列为空
计算树的深度算法
1. 如果一颗树只有一个节点,它的深度是1;
2. 如果根节点只有左子树而没有右子树,那么二叉树的深度应该是其左子树的深度加1;
3. 如果根节点只有右子树而没有左子树,那么二叉树的深度应该是其右树的深度加1;
4. 如果根节点既有左子树又有右子树,那么二叉树的深度应该是其左右子树的深度较大值加1;
public static int getDepth(BinaryTree.Node node){
if(node == null){ //先写节点为空的情况
return 0;
}
int lDepth = getDepth(node.lChild); //左子树的深度
int rDepth = getDepth(node.rChild); //右子树的深度
return lDepth>rDepth? lDepth+1:rDepth+1;
}
注:(倒过来分析)
节点无,深度为0
节点为1,深度为1
左右子树深度相同,深度+1
左右子树深度不同,最大深度+1
递归分析:画一个简单的树,从最低层一个个计算,怎么递归得到最终值
计算二叉树中叶节点个数
public static int getLeafNodeNum(BinaryTree.Node node){
if(node==null){
return 0;
}
if(node.rChild==null&&node.lChild==null){
return 1;
}
return getLeafNodeNum(node.rChild)+getLeafNodeNum(node.lChild);
}
排序算法