一 .前言
1.什么是数据结构
数据结构是计算机存储、组织数据的方式。数据结构是指相互之间存在一种或多种特定关系的数据元素的集合。通常情况下,精心选择的数据结构可以带来更高的运行或者存储效率。数据结构往往同高效的检索算法和索引技术有关。
数据结构的逻辑结构: 指反映数据元素之间的逻辑关系的数据结构,其中的逻辑关系是指数据元素之间的前后关系,而与他们在计算机中的存储位置无关。逻辑结构包括:
- 集合:数据结构中的元素之间除了“同属一个集合” 的相互关系外,别无其他关系;
- 线性结构:数据结构中的元素存在一对一的相互关系;
- 树形结构:数据结构中的元素存在一对多的相互关系;
- 图形结构:数据结构中的元素存在多对多的相互关系。
数据的物理结构: 指数据的逻辑结构在计算机存储空间的存放形式.
2.为什么要学数据结构
1. 数据结构+算法 = 程序
2. 大厂的面试基本都会面试到算法.
下面正式开始................................................................................................
二 线性表
线性表是最简单、最基本、也是最常用的一种线性结构。 线性表是具有相同数据类型的n(n>=0)个数据元素的有限序列,通常记为: (a1,a2,… ai-1,ai,ai+1,…an) ,其中n为表长, n=0 时称为空表。简单来说就是数据元素的排序方式是线性的. 它有两种存储方法:顺序存储和链式存储,它的主要基本操作是插入、删除和检索等。
顺序存储方式(顺序表): 它是把逻辑上相邻的结点存储在物理位置相邻的存储单元里,结点间的逻辑关系由存储单元的邻接关系来体现,由此得到的存储表示称为顺序存储结构。顺序存储结构是一种最基本的存储表示方法,通常借助于程序设计语言中的数组来实现.
链接存储方法(链表):它不要求逻辑上相邻的结点在物理位置上亦相邻,结点间的逻辑关系是由附加的指针字段表示的。由此得到的存储表示称为链式存储结构,链式存储结构通常借助于程序设计语言中的指针类型来实现
三 顺序表
基本思想:元素的存储空间是连续的.在内存中是以顺序存储,内存划分的区域是连续的,顺序表是顺序存储的线性表
代表: ArrayList,Vector,数组,Student[] stus = new Student(){new Student(),new Student(),........}
在某个索引位置添加元素
public void add(int index, E element) {
if (index > size || index < 0)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
//动态扩容 当数组元素超出时 增加0.5倍
ensureCapacityInternal(size + 1); // Increments modCount!!
//将索引处后面数据向后移动一位
System.arraycopy(elementData, index, elementData, index + 1,
size - index);
elementData[index] = element;
size++;
}
删除某个索引处元素:
public E remove(int index) {
if (index >= size)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
modCount++;
E oldValue = (E) elementData[index];
int numMoved = size - index - 1;
if (numMoved > 0)
//将数组中的元素从指定位置前移动一位
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
elementData[--size] = null; // clear to let GC do its work
return oldValue;
}
所以,arraylist的优缺点:
优点:
- 查改效率高:直接通过下标获取元素,直接通过下标设置元素。因为数组元素在物理上是连续的,知道其中一个的内存地址,就可以推算出其他元素的内存地址。
- 后面增删效率高:每次增加或删除,都需要移动指定位置后面的元素,所以增删的位置越靠前,需要移动的元素就越多,效率就越低;反之,效率就越高。
缺点:
- 前面增删效率低
四.链表
链表是链式存储的线性表.包括单链表.单链循环表,双链表,双链循环表
1.单链表
链表的逻辑关系是由节点的指针决定的,节点的指针只指向下一个节点的链表就是单链表。
单链表的应用:MessageQueue。
2.单链循环表
将单链表中终端结点的指针端由空指针改为指向头结点,就使整个单链表形成一个环,这种头尾相连的单链表称为单循环链表,简称循环链表。
3.双链表
双链表有2个指针,next指针指向上一个节点,pre指针指向下一个节点。
双链表的应用:LinkedList。
4.双链循环表
双链循环表是闭环的双链表,它的尾节点的next指针指向头节点,它的头结点的pre指针指向尾节点。
五、单链表
1.单链表的实现
单链表是由一系列节点组成,节点有数据域和指针域 ,例如Message就是一个节点,MessageQueue就是单链表
MessageQueue插入一个节点,主要是
boolean enqueueMessage(Message msg, long when) {
.....
Message prev;
//for 循环是找到message要插入的位置
for (;;) {
prev = p;
p = p.next;
if (p == null || when < p.when) {
break;
}
if (needWake && p.isAsynchronous()) {
needWake = false;
}
}
//将message插入到messageQueque中
msg.next = p; // invariant: p == prev.next
prev.next = msg;
}
MessageQueue删除一个节点
Message next(){
.....
synchronized (this) {
// Try to retrieve the next message. Return if found.
final long now = SystemClock.uptimeMillis();
Message prevMsg = null;
Message msg = mMessages;
if (msg != null && msg.target == null) {
// Stalled by a barrier. Find the next asynchronous message in the queue.
do {
prevMsg = msg;
msg = msg.next;
} while (msg != null && !msg.isAsynchronous());
}
if (msg != null) {
if (now < msg.when) {
// Next message is not ready. Set a timeout to wake up when it is ready.
nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
} else {
// Got a message.
mBlocked = false;
if (prevMsg != null) {
prevMsg.next = msg.next;
} else {
mMessages = msg.next;
}
msg.next = null;
if (DEBUG) Log.v(TAG, "Returning message: " + msg);
msg.markInUse();
return msg;
}
} else {
// No more messages.
nextPollTimeoutMillis = -1;
}
}
自己手写实现
public class SingleLinkedList<E> { //头节点 private Node<E> first; //尾节点 private Node<E> last; //单链表长度 private int size; //查询 private Node<E> node(int index){ Node<E> resultNode = first; for (int i = 0; i < index; i++) { resultNode = first.next; } return resultNode; } //增加在尾部 private void add(E e){ Node<E> newNode = new Node<>(e,null); Node<E> temp = last; if (temp==null){ first = newNode; last = newNode; }else { temp.next = newNode; } size++; } //增加在某个位置 private void add(E e,int index){ if (index<0||index>size){ throw new IndexOutOfBoundsException(); } if (index==size){ add(e); }else { Node<E> nextNode = node(index); Node<E> preNode = node(index - 1); Node<E> newNode = new Node<>(e,nextNode); preNode.next = newNode; } size++; } //删除索引处的元素 private void remove(int index){ if (index<0||index>size){ throw new IndexOutOfBoundsException(); } Node<E> target = node(index); Node<E> next = target.next; if (index==0||index==size){ if (index==0){ //删除的元素刚好是第0个元素 first = target.next; }else { //集合中只有一个元素 if (node(index-1)==null){ first=null; last=null; size=0; }else { //将最后一个元素置为null node(index-1).next=null; size--; } } }else { node(index-1).next = target.next; size--; } } class Node<E> { private E item; private Node<E> next; public Node(E item, Node<E> next) { this.item = item; this.next = next; } } }
2.单链表的优缺点
优点:
- 前面查改效率高:每次查询和修改都要遍历整个链表,所以查改位置越靠前,效率越高;反之,效率越低。
- 前面增删效率高:增加和删除时,只需要改变上一个节点的指针。但是获取上一个节点,要遍历整个链表。所以增删的位置越靠前,效率越高;反之,效率越低。
缺点:
- 后面查改效率低;
- 后面增删效率低。
3.MessageQueue为什么使用单链表
MessageQueue是用来存放延迟消息的,最先发送的消息需要放在链表的最前面,每次取消息就只需要去最前面的数据。一般新插入的消息,都是放在链表的前面。所以查询和插入操作的位置都很靠前,刚好符合单链表的优点。
4.单链表的倒序
单链表由于只有首节点,所以倒序比较麻烦。这里有两种方法
//循环倒序 public void reset() { Node<E> curr = first; Node<E> last = null; while (curr != null) { Node<E> temp = curr.next; curr.next = last; last = curr; curr = temp; } first = last; } //递归倒序 public Node<E> reset(Node<E> curr) { //结束条件 if (curr == null || curr.next == null) { return curr; } //循环条件 Node<E> tail = reset(curr.next); //循环内容 curr.next.next = curr; curr.next = null; return tail; }
六、双链表
1.双链表的实现
这里我们手写实现一个双链表。
public class LinkedList<E> {
/**
* 结点
*/
private static class Node<E> {
E item;
Node<E> prev;
Node<E> next;
public Node(Node<E> prev, E item, Node<E> next) {
this.item = item;
this.prev = prev;
this.next = next;
}
}
public LinkedList() {
}
//头节点
Node<E> first;
//尾节点
Node<E> last;
//大小
int size;
/**
* 添加数据在最后
*/
public void add(E e) {
linkLast(e);
}
/**
* 添加到最后
* @param e
*/
private void linkLast(E e) {
Node<E> newNode = new Node<E>(last, e, null);
Node<E> l = last;
last=newNode;
if(l==null){
first=newNode;
}else {
l.next = newNode;
}
size++;
}
/**
* 查找位置
*/
public E get(int index){
if(index<0 || index>size){
return null;
}
return node(index).item;
}
/**
* 获取index位置上的节点
*/
private Node<E> node(int index){
//如果index在整个链表的前半部分
if(index<(size>>1)){ //1000 100 10
Node<E> node=first;
for (int i = 0; i < index; i++) {
node=node.next;
}
return node;
}else{
Node<E> node=last;
for (int i = size-1; i > index; i--) {
node=node.prev;
}
return node;
}
}
/**
* 添加数据在index位置
*/
public void add(int index,E e) {
if(index<0 || index>size){
return ;
}
if(index==size){
linkLast(e);
}else{
Node<E> target=node(index);// index=2
Node<E> pre=target.prev;
Node<E> newNode=new Node<E>(pre,e,target);
if(pre==null){
first=newNode;
target.prev = newNode;//4
}else {
pre.next = newNode;//3
target.prev = newNode;//4
}
size++;
}
}
/**
* 删除元素
*/
public void remove(int index){
Node<E> target=node(index);
unlinkNode(target);
}
private void unlinkNode(Node<E> p) {//index=2
Node<E> pre=p.prev;
Node<E> next=p.next;
if(pre==null){
first=p.next;
}else{
pre.next=p.next;
}
if(next==null){
last=p.prev;
}else{
next.prev=p.prev;
}
size--;
}
}
2.双链表的优缺点
优点:
- 两端查改效率高:双链表遍历时,是从两端开始,所以查改位置越靠两端,效率越高;反之,效率越低。
- 两端增删效率高:双链表增删时,只需改变前一个节点和后一个节点的指针。但是获取上一个节点,要遍历整个链表。所以增删的位置越靠两端,效率越高;反之,效率越低。
- 直接删除节点效率:如果删除的时节点,那么就可知直接其前后节点,所以需改变前一个节点和后一个节点的指针。
缺点:
- 中间查改效率低;
- 中间增删效率低;
- 内存比单链表高:因为有2个指针。