什么是数据结构
简单来说就是数据元素和定义在数据元素上的一组操作的一个集合。
其中,基于数据的操作,需要保证操作后的数据仍然满足原有关系。
研究重点:存储数据间的关系(顺序映像、非顺序映像)
顺序映像:以数据元素之间的物理位置紧邻来表示关系
非顺序映像:借助指示元素存储地址的“指针”来表示关系
物理关系:逻辑关系在计算机中的表示
线性表
顺序表
操作主要有:增、删、查、改
优势:随机访问方便(在相同的时间里访问线性表中任意一个元素,每个元素位序唯一,类型数组下标)
难点:在操作元素后,维护原本的一一对应的元素关系。
对于 n个 元素线性表,有n+1个合法插入位置 / 有n个合法删除位置。
实现方式:
存储元素:数组 elements[] / size 表示数组中元素个数 / 用接口来接收 增,删,查,改的方法
(接口在某种程度上可以看成是一种 抽象协议 )
代码思路:
1.设置接口,定义增删查改方法和重写toString()方法
2.定义 类LineList 实现接口,类中成员变量:数组elements[] ,元素个数int size ,存储上限MAX_VALUE
3.在类中重写接口的方法:
插入:1)判断插入位置是否合法
2)判断数组容量,不够就需要扩容(重点)
3)移动数组,空出插入位置
4)插入元素,元素个数 + 1
5)返回 插入成功
扩容的步骤: 1.先扩容为原数组的1.5倍
2.判断扩容后的数组是否大于用户指定的值
3.再判断扩容后的数组大小是否越界,如果超过了数据类型的最大值,就会出现符号反转 ,如int 型 ,2147483647 + 1 = - 2147483648,如果越界,就将类型的最大值赋给数组空间长度。
删除:1)判断删除位置是否合法
2)判空
3)用一个参数接收要删除的元素,作为返回值
4)将要删除的元素的后面所有元素整体往前移动一位,覆盖掉原来的待删除元素(System.arraycopyOf(),后面会提)
5)将数组最后一个元素置为null,因为最后一个元素已经前移了一位,最后一个应该不存在了
6)元素个数 - 1,返回待删除元素
修改:1)判断查找位置是否合法,判断是否数组为空
2)找到位序,直接修改即可
访问:1)判断访问位置是否合法,判断数组是否为空
2)找到位序,直接返回即可
线性表 顺序映像 代码如下
接口
public interface LinearList { //添加元素 /** * * @param e 带插入到线性表中的元素 * @param index 待插入元素插入到线性表中的位序 * @return 表示本次插入操作是否成功 */ boolean insert(String e, int index); /** * 将带插入元素插入到当前线性表中,表尾 * * @param e 待添加元素 * @return 添加操作是否成功 */ boolean insert(String e); //删除元素 /** * * @param index 要删除当前线性表中,哪个位序的元素 * @return 待删除元素的元素值 */ String delete(int index); /** * * @param e 在线性表中,待删除的元素的元素值和该参数的元素值相同 * @return 当前的删除操作是否成功 */ boolean delete(String e); //修改 /** * * @param index 指明线性表中待修改的元素 * @param newValue 指明线性表中待修改的元素,修改之后的值 */ void set(int index, String newValue); //查找 /** * * @param e 待查找的元素 * @param fromIndex 从哪个地方开始查找 * @return */ int indexOf(String e, int fromIndex); /** * * @return 当前线性表,所有元素的字符串表示 [1,2,3] [] [1] */ String toString(); }
线性表主体代码
public class Mylinelist implements LinearList { //设置参数 //数组 private String[] element; //当前数组元素个数 private int size; //线性表存储上限 private final static int MAX_ARRAY_SIZE = Integer.MAX_VALUE; //设置一个初始值 private final static int DEFAULT_INTI_SIZE = 10; public Mylinelist() { //初始化线性表中,实际存放元素的数组 element = new String[DEFAULT_INTI_SIZE]; } //*********用户传入的数组容量********* public Mylinelist(int capacity) { if(capacity < 0 || capacity > MAX_ARRAY_SIZE){ throw new IllegalArgumentException("超出存放范围"); } //初始化用户传入的数组容量大小的数组 element = new String[capacity]; } @Override public boolean insert(String e, int index) { //判断index 合法性 if(index < 0 || index > size){ throw new IllegalArgumentException("不合法的插入位置"); } //判断数组容量问题 ensurInsert(size + 1); //插入 element[index] = e; //插入后 size++; return true; } private void ensurInsert(int capacity) { //扩容 if(capacity > element.length){ int oldcapacity = element.length; int newcapacity = oldcapacity + (oldcapacity >> 1); //如果数组超过最大值导致变成负数 if(newcapacity - capacity < 0){ newcapacity = capacity; } //数组移动 element = Arrays.copyOf(element,newcapacity); } } @Override public boolean insert(String e) { return false; } @Override public String delete(int index) { //判断删除合法性 if(index < 0 || index > size - 1){ throw new IllegalArgumentException("删除位置不合法"); } //保留要返回的删除元素 String oldNum = element[index]; //元素前移 if(index != size - 1){ System.arraycopy(element,index + 1, element, index,size - index - 1); } //最后一个元素设置为null element[size - 1] = null ; //元素个数少一 size--; //返回删除元素值 return oldNum; } @Override public boolean delete(String e) { //判空比较 if (element == null) { throw new NullPointerException("无法删除"); } //遍历比较符合的第一个元素 for (int i = 0; i < size; i++) { if (Objects.equals(e, element[i])) { int copyNum = size - i - 1; if (copyNum != 0) { System.arraycopy(element, i + 1, element, i - 1 , size - i ); //将最后一个置为空 element[size] = null; //size-- size--; //删除成功 return true; } } } //比较不成功 return false; } @Override public void set(int index, String newValue) { //检查位序 if(index < 0 || index > size){ throw new IllegalArgumentException("修改位序不合法"); } //修改元素值 element[index] = newValue; } @Override public int indexOf(String e, int fromIndex) { for (int i = fromIndex; i < size ; i++) { if(Objects.equals( e, element[i])){ return i; } } return -1; } @Override public String toString() { if(size == 0){ return "[]"; } StringBuilder builder = new StringBuilder(); builder.append("["); for (int i = 0; i < size; i++) { builder.append(element[i]); if(i == size -1){ builder.append("]"); break; } builder.append(","); } return builder.toString(); } }
测试
public class TestList { public static void main(String[] args) { Mylinelist mylinelist = new Mylinelist(); for (int i = 0; i < 10 ; i++) { mylinelist.insert(i + "", i);//" "和 "\t"均算成字符 } System.out.println(mylinelist.toString()); //正常运行 删除 mylinelist.delete(6);//index是数组下标(0开始) System.out.println(mylinelist.toString()); //正常运行 查找 System.out.println( mylinelist.indexOf("4",2)); //正常运行 修改 mylinelist.set(5,"15"); System.out.println(mylinelist.toString()); } }
其中有几个方法需要注意:
- 数组元素整体复制移动:System.arraycopy( 原数组 , 要移动的起始下标index,新数组 , 要放入的起始位置 , 复制元素的个数 ) eg.System.arraycopy(elements, index , elements ,index+1,size - index)-----完成同一个数组中的元素整体后移一位
- 数组扩容:Arrays.copyOf( 新数组,扩容数组长度) eg.elements = Arrays.copyOf(elements , size + 1) ---完成原数组的扩容操作
- 可变字符串序列:StringBuilder 类(假如对象 builder ),有一个方法:append----用于不断添加字符 eg.builder.append("a");builder.append("b"); 最后输出就是 ab
还有一个方法: deleteCharAt(int index)----用于删除可变字符串 某一个位置的字符
4.设置数组的最大容量的方法:private final static int MAX_ARRY_SIZE = Integer.MAX_VALUE;
Integer.MAX_VALUE 就是 int 类型的最大值,同理可以设置 Integer.MIN_VALUE , Short.MAX_VALUE...........
链表
属于非顺序映像,必须顺序访问,可以实现随机存取。
非顺序映像一定不以元素的物理位置来表示元素之间的关系。
实现方式:节点类Node 中存储一个节点元素,String item,
一个指向下一个节点的引用 Node next
一个构造方法,初始化 结点元素和指向下一个结点的引用
每个结点类的对象 就可以看成链表中的 一个结点
代码思路:
1.定义接口,插入add(String e , int index)删除、查找、修改
2.定义成员变量:定义一个指向第一个节点的引用 Node first ; 一个指向最后一个节点的引用 Node last ;链表中元素个数 size
3.实现接口:
插入:1)判断插入位置的合法性 rangeCheck(index)
2)找到指定位序的前一个节点( preNode方法):先设定一个Node node来接收first引用,然后遍历,不断将node = node.next ,直到插入位序的前一个节点,此时 就将这个node给到Node pre,即Node pre = preNode( index ),此时,pre引用就表示了插入位序的前一个节点引用。
3)插入元素(Link(Node pre,String e) 方法):判断pre ==null,则将插入的节点给到 first ,如果size ==0,则将last =first,如果链表不空的话,就
Node node = pre.next( node 表示插入位序的下一个位序的结点的引用,pre.next代表插入位序的前一个结点的指向,指向了一个实例node,即完成了解开原本链表的插入位序结点和前一个结点的链接----pre.next)
然后 pre.next = new Node(e , node)( 其中new Node(e,node)表示新建一个Node类的实例,因为Node中的node表示下一个结点的引用,所以指向了node,也就是之前pre.next所指向的结点,即原链表的插入位序所在的结点)
照这个思路,单链表的插入操作就非常好理解了。
删除:1)判断删除链表是否合法 [ 0--size -1]
2)找到删除结点的前驱 Node pre =preNode( index )
3)判断删除的是否是表头结点,是表头,则first = first.next,判断是否只有一个节点,如果删除了first
4 )非表头时的删除操作 先找个值来接收待删除节点int oldValue = cur.item
Node cur = pre.next; pre.next = cur.next ; if(pre.next == null){last = pre}
查找:遍历链表就可以了
修改:遍历找到当前节点直接修改item。
代码实现如下
public class MyLinkedList implements LinearList { //1. 第一个(节点)的引用 private Node first; //2. 定义指向当前链表中最后一个节点的引用 private Node last; //3. 当前链表中包含的元素个数 private int size; //定义无参构造方法 public MyLinkedList() { } @Override public boolean add(String e, int index) { //1. index 插入的位序是否合法 [0,size] rangeCheckForAdd(index); //2.找到指定位序的前一个节点 Node pre = preNode(index); //3.向链表中插入带插入元素 link(pre, e); return true; } private void link(Node pre, String e) { if(pre == null) { // 要在当前链表中的第一个位置插入元素 first = new Node(e, first); if(size == 0) { //在插入当前元素之前,链表为空 //此时如果插入这个节点,那么这个节点既是当前链表中的第一个节点又是当前链表中的最后一个节点 last = first; } } else { //当指定的插入位序,不是第一个位序的时候 有前驱 //插入位序,的下一个位序的结点的引用 Node node = pre.next; //构造待插入元素的结点,并且,该节点插入到链表 pre.next = new Node(e, node); if(node == null) { last = pre.next; } } size++; } /* 找到指定位序index,指明的前一个位序的结点的引用 */ private Node preNode(int index) { if(index == 0) { // 当前要插入的元素,是整个线性表中第一个元素的位置 return null; } //对于指定位序的元素,查找其前驱 //找前驱 Node node = first; for(int i = 1; i < index; i++) { node = node.next; } return node; } private void rangeCheckForAdd(int index) { if(index < 0 || index > size) { throw new ArrayIndexOutOfBoundsException("Index: " + index + ", " + "Size: " + size); } } //其实质实现的是尾插法 @Override public boolean add(String e) { //找带插入元素的前驱 last link(last, e); return true; } //实现头插法 public boolean addHead(String e) { // link(null, e); return true; } @Override public String remove(int index) { // 判断删除的合法位序 [0, size - 1] rangeCheck(index); //找到待删除节点的前驱 Node pre = preNode(index); String oldValue = unlink(pre); return oldValue; } private String unlink(Node pre) { //当pre为null的时候,只有一种情况,就是在当前线性表不为空的情况下,删除的是表头结点 String oldValue = null; if(pre == null) { //删除表头元素 oldValue = first.item; first = first.next; if(first == null) { //在删除表头结点之前,线性表中只包含一个表头结点 last = null; } } else { //当要删除的结点不是表头结点的时候 //表示待删除节点的引用 Node cur = pre.next; //待删除节点的元素值 oldValue = cur.item; //从链表中删除,待删除结点 pre.next = cur.next; if(pre.next == null) { //删除的是表尾节点,因此修改last的值 last = pre; } } size--; return oldValue; } private void rangeCheck(int index) { if(index < 0 || index >= size) { throw new ArrayIndexOutOfBoundsException("Index: " + index + ", Size: " + size); } } @Override public boolean remove(String e) { if (size == 0) { throw new ArrayIndexOutOfBoundsException(""); } Node pre = null; for(Node node = first; node != null; node = node.next) { if(Objects.equals(e, node.item)) { unlink(pre); } pre = node; } return true; } @Override public void set(int index, String newValue) { //1. 检查合法位序 rangeCheck(index); //2. 找到index指明的位序的,节点 Node node = preNode(index + 1); //3.修改 node.item = newValue; } @Override public int indexOf(String e) { // 当链表为空的时候,根据需求,也可以抛出一个自定义的编译时异常 // if(size == 0) { // throw new ArrayIndexOutOfBoundsException(""); // } int index = 0; for(Node node = first; node != null; node = node.next) { if(Objects.equals(e, node.item)) { return index; } index++; } return -1; } private class Node { //表示存储的数据元素的值 String item; //指向下一个节点的引用 Node next; public Node(String item, Node next) { this.item = item; this.next = next; } } /* 让使用者获取,当前线性表中的元素个数 */ public int size() { return size; } /* 判断当前线性表是否为空表 */ public boolean isEmpty() { return size == 0; } @Override public String toString() { if(size == 0) { return "[]"; } StringBuilder builder = new StringBuilder("["); //[ 1,2, good taste 消除了对边界条件的特殊判断 for(Node node = first; node.next != null; node = node.next) { builder.append(node.item); builder.append(','); } builder.append(last.item); builder.append(']'); return builder.toString(); } public String getHeadValue() { return first.item; } }
测试类:
public class TestList { public static void main(String[] args) { // MySquentialList mySquentialList = new MySquentialList(); // // //测试我的添加功能 // testAdd(mySquentialList); // // //测试删除功能 mySquentialList.remove("1"); System.out.println(mySquentialList.toString()); // // testRemove(mySquentialList); MyLinkedList myLinkedList = new MyLinkedList(); testLinkedAdd(myLinkedList); System.out.println(myLinkedList); insertLinkedAt(myLinkedList,"999", 1); System.out.println(myLinkedList); insertLinkedHead(myLinkedList, "2333"); System.out.println(myLinkedList); myLinkedList.remove(4); System.out.println(myLinkedList); } private static void testLinkedAdd(MyLinkedList myLinkedList) { for(int i = 100; i < 110; i++) {//循环遍历插入 myLinkedList.add(i+""); } } public static void testRemove(MySquentialList mySquentialList) { mySquentialList.add(null); System.out.println(mySquentialList.toString()); // mySquentialList.remove(null); // System.out.println(mySquentialList.toString()); mySquentialList.remove("1"); System.out.println(mySquentialList.toString()); } public static void testAdd(MySquentialList list) { for (int i = 0; i < 20; i++) { list.add(i + "", i); } System.out.println(list.toString()); } public static void insertLinkedAt(MyLinkedList list, String e, int index) { list.add(e, index); } public static void insertLinkedHead(MyLinkedList list, String e) { list.addHead(e); } }
操作受限的线性表(栈和队列)
栈:
特点:先进后出,合法插入位置和删除位置只有一个-----栈顶
可以类比JVM中的内存模型中的方法栈:栈帧(在调用方法时,产生栈帧,调用结束,销毁栈帧,后调用的方法先结束)
实现栈的代码思路
1.定义接口:入栈 boolean push(String s) ; 出栈String pop(); 访问栈顶元素 String peek();
2.定义成员变量:elements[] ,存放栈内元素 , int size 表示栈内元素个数 ;int top 表示栈顶指针,初始值为0; 存储上限Integer.MAX_VALUE ; 构造方法:可以让用户指定栈空间大小
3.实现接口
入栈:1)保证存储容量足够让元素入栈 ensureCapacity( mincapacity )如果不够就需要扩容
2)入栈:elements[ top ] = e ; top++;
其中重点是扩容。
出栈:1 ) top -- ; elements[ top ] = null ;
访问栈顶元素:1) 判断栈空
2) return elements[ top - 1]
4.也可以通过继承顺序映像/多态来完成入栈和出栈的操作(可以自己去实现)
代码如下
public class HomeWork_Stack implements Stack_1 { //成员变量 private String[] elements;//存放栈中元素值 private int top;//栈顶指针 private final static int MAX_VALUE =Integer.MAX_VALUE; //构造方法 HomeWork_Stack() { } HomeWork_Stack(int capacity) {//让用户指定 创建的初始数组大小 if(capacity < 0){ throw new ArrayIndexOutOfBoundsException("输入数值非法"); } elements = new String[capacity]; } @Override public boolean push(String s) { //入栈必定合法 //确保有足够的容量让元素入栈 ensureCapacity(top + 1); //元素入栈 add( s ); return true;//入栈成功 } private void add(String s) {//在栈顶添加元素 elements[top] = s; top++; } private void ensureCapacity(int minCapacity) { if(minCapacity - elements.length > 0 ){//如果输入的容量 大于 原数组的容量,扩容 grow(minCapacity); } } private void grow(int minCapacity) { int oldcapacity = elements.length;//先接收原数组容量 int newcCapacity = oldcapacity +(oldcapacity >> 1); if(newcCapacity < minCapacity){ newcCapacity = minCapacity;//如果扩容后的数组容量不到输入时的容量,则按输入的容量来扩容 } if(newcCapacity - MAX_VALUE > 0){// >0 表示已经超出最大范围 newcCapacity = hugeCapacity(minCapacity); } } private int hugeCapacity(int minCapacity) { if(minCapacity < 0){ throw new ArrayIndexOutOfBoundsException("容量超出范围"); } return MAX_VALUE;//如果扩容后超出范围,只能给到范围最大值 } @Override public String pop() {//出栈 //先判断栈是否为空 if( top == 0 ){//数组元素可以是null,所以不能判断为 elements[top] == null throw new ArrayIndexOutOfBoundsException("数组为空,无法出栈"); } //出栈,先保留要出栈元素 String oldValue = elements[top - 1]; elements[top - 1] = null; //指针下移 top--; return oldValue; } @Override public String peek() { //判断栈内是否有元素可访问 if(top == 0){ throw new NullPointerException("栈内无元素访问"); } //返回 访问栈顶元素 return elements[top - 1]; } public String toString(){ if(top == 0 ){ return "[]"; } StringBuilder builder = new StringBuilder(); builder.append("["); for (int i = 0; i < top - 1; i++) { builder.append(elements[i]); builder.append(","); } builder.append(elements[top - 1]); builder.append("]"); return builder.toString(); } }
测试:
public class TestStack { public static void main(String[] args) { HomeWork_Stack homeWork_stack = new HomeWork_Stack(10);//初始化数组空间 //添加元素 功能正常 homeWork_stack.push("1"); homeWork_stack.push("3"); homeWork_stack.push("5"); homeWork_stack.push("7"); System.out.println("入栈后栈空间" + homeWork_stack.toString()); //删除元素 功能正常 homeWork_stack.pop(); System.out.println("出栈后栈空间" + homeWork_stack.toString()); //查找元素 功能正常 ,没有删除元素 System.out.println(homeWork_stack.peek()); System.out.println(homeWork_stack.toString()); } }
队列
特点:添加元素在队尾,删除元素在队头。
用处:如树的广度优先遍历,需要借助队列模型;栈在非递归的深度优先遍历中,也需要借助队列模型
非顺序印象的实现思路:
1)定义接口:添加元素, boolean Queue(String e) 尾插法;String deQueue() 删除队头;String peek() 访问队头元素
String peek(){ if isEmpty(return){return null} return first.item; }
2)代码复用,继承接口和链表,调用其尾插法
顺序印象的代码实现
循环队列
实现步骤:1)定义接口 : 入队,出队,查找队头元素
2)定义成员变量:1.实际存放队列元素的数组 elements[] ; 两个指针 front / rear ,尾指针的位置移动: rear = (rear+1) % 数组长度 ; 定义队列中元素个数 size ( size =(rear - front+elements.length)% elements.length ) ; 常量:最大范围,初始数组容量。
3)定义构造方法,接收用户传来的参数,可以自定义队列大小。
4)入队:ensureCapacity 确保容量,不够就要扩容,循环队列的扩容标志为