概述
数据结构:是相互之间存在一种或多种特定关系的数据元素的集合。(通俗来讲就是数据在计算机上是如何组织和存储)
按照数据结构中元素的相互关系可以分为4类:集合,线性结构,树形结构,图或网状结构。
1.集合:结构中的元素除了同属于一个集合外无其他关系。
2.线性结构:线性元素之间存在一个对一个的关系。
3.树形结构:结构中元素之间存在一对多的关系。
4图或网状结构:结构中的元素存在多对多的关系。
以下将分为三个部分来说明常见的数据结构
1.线性表(数组,链表,栈,队列)以及特殊的散列表(也叫哈希表)
2.树(二叉树,二叉排序树,二叉搜索树,平衡二叉树,红黑树,最优二叉树(哈夫曼树),B树等)
3.图(最小生成树,最短路径问题)
线性表
线性表是n个数据元素的有限序列。
数组
具有一段连续(物理上或者逻辑上)的内存,存储同一种类型数据的集合,数组其实就是一个容器。
java实现数组
数组包含的方法:
1.增:依次插入元素(add)
2.删:根据数组下标删除(deleteByIndex),根据值删除(deleteByValue)
3.改:根据数组下标修改值(modify)
4.查:查询数组中是否包含该值(searchByValue)
5.辅助函数:打印数组(printArr)
public class MyArray {
private int arr[];
/**
*currentIndex:当前数组最后一个元素的索引
*capacity:数组的容量
*/
private int currentIndex = -1;
private int capacity = 0;
private boolean flag = false;
/**
* 构造方法,默认构造数组大小为10
*/
public MyArray() {
arr = new int[10];
capacity = 10;
}
public MyArray(int size) {
arr = new int[size];
capacity = size;
}
/**
* 1.增(依次插入元素)
*/
public void add(int value) {
if (currentIndex < capacity - 1) {
arr[++currentIndex] = value;
} else {
System.out.println("数组已满,无法继续插入!");
}
}
/**
* 2.删(通过下标删除,判断索引如果越界抛出异常,删除成功返回true,失败返回false)
*
* @param index 待删除元素的数组下标
*/
public boolean deleteByIndex(int index) {
if (index < 0 || index >= capacity) {
throw new ArrayIndexOutOfBoundsException();
} else {
for (int i = index; i <= currentIndex - 1; i++) {
arr[i] = arr[i + 1];
}
currentIndex--;
flag = true;
return flag;
}
}
/**
* 2.删(通过数值删除,如果数组中包含值就删除,不包含就打印信息)
*
* @param value:待删除的元素
*/
public boolean deleteByValue(int value) {
int index;
if ((index = this.searchByValue(value)) == -1) {
System.out.println("数组中不包含" + value);
flag = false;
} else {
this.deleteByIndex(index);
flag = true;
}
return flag;
}
/**
* 3.改
*/
public boolean modify(int index, int value) {
if (index < 0 || index >= capacity) {
throw new ArrayIndexOutOfBoundsException();
} else {
arr[index] = value;
flag = true;
}
return flag;
}
/**
* 4.查(查询值是否存在数组中,存在返回值的下标,不存在返回-1)
*/
public int searchByValue(int value) {
int i = 0;
for (; i < capacity; i++) {
if (arr[i] == value) {
break;
}
}
if (i == capacity) {
i = -1;
}
return i;
}
/**
* 5.打印数组
* */
public void printArr() {
System.out.println("数组容量:" + capacity + ", 当前元素数量:" + (currentIndex + 1));
System.out.print("arr={");
for (int i = 0; i <= currentIndex; i++) {
if (i == currentIndex) {
System.out.println(arr[i] + "}");
} else {
System.out.print(arr[i] + ",");
}
}
}
数组的特点:
1.数组中储存的元素必须是相同的类型。
2.长度固定,在内存中占据连续空间。
3.存值取值快,只需提供相应的索引值,时间复杂度为O(1),增删需要移动插入元素后元素,时间复杂度为O(N),无序数组的查找必须遍历数组元素值,因此时间复杂度为O(N)。
数组适用情况:
储存的元素都是相同的类型,且数组大小固定,存取值较多,增删,查找较少的情况。
有序数组
有序数组数组就是指数组中的元素是按一定的规则排列的,这样在查找时可以利用二分查找极大提升了查找的效率。有序数组的缺点也相当明显,当插入一个元素时,需要确定待插入元素的下标,然后对其后的元素进行后移一位处理,所以插入效率不高。
需要修改数组实现中的添加add,二分查找searchByValue方法:
public void add(int value) {
int location = -1;
// 判断数组是否已满
if (currentIndex < capacity - 1) {
// 数组为空时,索引为0
if (currentIndex == -1) {
location = 0;
} else {
// 遍历数组,找到第一个大于value的值,此时i即为插入的下标
for (int i = 0; i <= currentIndex; i++) {
if (value < arr[i]) {
location = i;
break;
}
}
// 如果没有找到,说明value应该插入到数组末尾
if (location == -1) {
location = currentIndex + 1;
}
}
// 依次将location后面的元素后移一位
for (int i = currentIndex; i >= location; i--) {
arr[i + 1] = arr[i];
}
// 插入value更新currentIndex
arr[location] = value;
currentIndex++;
} else {
System.out.println("数组已满,无法继续插入!");
}
}
/**
* 4.查找方法,使用二分查找,如果找到就返回下标值,如果没有就返回-1
*/
public int find(int target) {
int index = -1;
int begin = 0;
int end = currentIndex;
while (true) {
int mid = (begin + end) / 2;
// 如果相等说明找到了
if (arr[mid] == target) {
index = mid;
break;
// 如果mid和begin重合,说明区间内只含两个元素,只需比较arr[end]和target,退出循环
} else if (mid == begin) {
if (arr[end] == target) {
index = end;
}
break;
// 如果arr[mid]和target不相等,且mid与begin不相等说明此时区间内至少含有三个数,
// 判断arr[mid]与target的大小,更新区间
} else {
if (arr[mid] > target) {
end = mid;
} else {
begin = mid;
}
}
}
return index;
}
注:
实现数组有序的是插入方法,在插入的时候先找到该元素合适的下标再进行插入操作。
因为数组已经有序,查找方法用二分查找实现。
因为数组已经有序,删除一个数后,剩下的数组依然有序。
对数组值进行修改相当于先删除待修改的值,然后调用add方法插入修改后的值。
有序数组特点:
存取值快,时间复杂度O(1);使用二分查找,查找的时间复杂度为O(logN);插入,修改操作需要移动元素,时间复杂度为O(N)
适用情况:查找,存取值频繁,插入,删除操作较少。
链表
链表中的每个节点都包含了数据域和引用(指针域),其中数据域储存了数据,而引用(指针域)指向了下一个节点所在的位置,这样节点就通过引用(指针域)连接起来构成了链表。
单链表
单链表中每个节点均指向其后一个节点,而后面节点没有它前驱节点的信息,头节点无前驱节点,尾节点无后继节点(指向null),也可以理解为单链表是从头到尾单方向传递的。
链表示意图:
单链表的操作:
1.插入
插入分为三种情况,即头节点前插入,尾节点后插入,其他位置插入
头节点前插入
尾节点后插入
其他节点插入
2.删除
删除节点分为三种情况,删除头节点,删除尾节点,删除其他位置节点
删除头节点
删除尾节点
删除其他节点
单链表Java实现
单链表包含的方法:
1.增:在第一个节点前面添加节点(addFirst),在指定节点前(后)插入新节点(insAfter,insBefore)。
2.删:删除第一个节点(delFirst),删除指定的节点(delByData)
3.改:修改指定节点的数值(modifyByValue)
4.查:查找值对应的节点(SearchByData),查找值对应的父节点(getParent)
5.辅助函数:判断链表是否为空(isEmpty),链表是否包含该数值(containsData),打印链表(PrintLList)
//封装节点
public class LNode {
public int data;
public LNode next;
public LNode(int data) {
this.data = data;
}
public LNode() {
}
}
//单链表实现
public class MyLinkedList {
private LNode first;
public MyLinkedList() {
first = null;
}
/**
* 1.增,向头节点前插入节点,成功返回节点node
*/
public LNode addFirst(LNode node) {
node.next = first;
first = node;
return node;
}
/**
* 1.增,在target后插入node节点,插入成功返回node节点!
*/
public LNode insAfter(int target, LNode node) throws Exception {
LNode temp;
LNode cur;
cur = this.searchByData(target);
temp = cur.next;
cur.next = node;
node.next = temp;
return node;
}
/**
* 1.增,在target前插入node节点,插入成功返回node节点!
*/
public LNode insBefore(int target, LNode node) throws Exception {
LNode cur;
LNode pre;
cur = this.searchByData(target);
pre = this.getParent(target);
pre.next = node;
node.next = cur;
return node;
}
/**
* 2.删,删除第一个节点,成功返回删除的节点对象
*/
public LNode delFirst() throws Exception {
if (this.isEmpty()) {
throw new Exception("链表为空!不能删除!");
}
LNode temp = new LNode();
temp = first;
first = first.next;
return temp;
}
/**
* 2.删,删除指定的节点,成功返回被删除的节点
*/
public LNode delByData(int target) throws Exception {
LNode cur;
LNode pre;
cur = this.searchByData(target);
pre = this.getParent(target);
pre.next = cur.next;
return cur;
}
/**
* 3.改,修改指定节点的值
*/
public LNode modifyByValue(int target, int value) throws Exception {
LNode cur;
cur = this.searchByData(target);
cur.data = value;
return cur;
}
/**
* 4.查,由data值查找LNode的位置
*/
public LNode searchByData(int target) throws Exception {
LNode temp = first;
while (temp != null) {
if (temp.data == target) {
break;
}
temp = temp.next;
}
return temp;
}
/**
* 4.查,查找target元素对应的父节点,找到就返回父节点,不存在就抛出异常!
*/
public LNode getParent(int target) throws Exception {
if (!this.containsData(target)) {
throw new Exception("链表中不包含该值对应的节点!");
}
LNode pre = first;
// 第一个元素无parent
while (pre.next != null) {
if (pre.next.data == target) {
break;
}
pre = pre.next;
}
return pre;
}
/**
* 判断链表是否为空,头节点为空则链表为空。
*/
public boolean isEmpty() {
return first == null;
}
/**
* 打印链表
*/
public void printLList() {
LNode ln = first;
System.out.print("{");
while (ln != null) {
if (ln.next == null) {
System.out.println(ln.data + "}");
break;
}
System.out.print(ln.data + ",");
ln = ln.next;
}
}
/**
* 链表是否包含node
*/
public boolean containsData(int target) {
LNode ln = first;
while (ln != null) {
if (ln.data == target) {
return true;
}
ln = ln.next;
}
return false;
}
单链表特点
插入(链表末尾插入),删除快,只需要操插入和删除节点前后节点的引用即可完成,数据复杂度为O(1)。
存取值,查找慢,由于单链表后续节点无其前驱节点的信息,所以存取值和查找都必须从头节点开始依次向后寻找,时间复杂度为O(N)。
适用情况:增删较多,查询少,存取少。
双端链表
与单链表比较相似,不同的是在他的头节点加入了一个对尾节点的引用。(注:双端链表与双向链表是不同的)
特点:与单链表最大的不同是保存了尾节点的引用,不需要从头遍历获取尾节点,方便在最后的节点进行插入操作(如队列)。
双端链表的Java实现
对比单链表增加了一个尾节点引用last,需要修改单链表addFirst,delFirst方法,增加addLast,delLast(效率不高)方法,其他单链表方法也可以使用。
public class DoubleSideLinkedList {
//首尾节点的引用
private LNode first;
private LNode last;
/**
* 1.增,向头节点前插入节点,成功返回节点node
*/
public LNode addFirst(LNode node) {
if (this.isEmpty()) {
last = node;
}
node.next = first;
first = node;
return node;
}
/**
* 1.增,在尾部插入节点
*/
public LNode addLast(LNode node) {
if (this.isEmpty()) {
first = node;
}
last.next = node;
last = node;
node.next = null;
return node;
}
/**
* 2.删,删除第一个节点,成功返回删除的节点对象
*/
public LNode delFirst() throws Exception {
LNode temp = new LNode();
if (this.isEmpty()) {
throw new Exception("链表为空!不能删除!");
} else if (first == last) {
// 如果只有一个节点
temp = first;
first = null;
last = null;
} else {
// 两个节点及以上
temp = first;
first = first.next;
}
return temp;
}
/**
* 2.删,删除尾节点
*/
public LNode delLast() throws Exception {
LNode temp = new LNode();
if (this.isEmpty()) {
throw new Exception("链表为空!不能删除!");
} else if (first == last) {
// 如果只有一个节点
temp = last;
first = null;
last = null;
} else {
// 两个节点及以上
LNode pre = this.getParent(last.data);
temp = last;
pre.next = null;
last = pre;
}![在这里插入图片描述](https://img-blog.csdnimg.cn/20200511004349426.png#pic_center)
return temp;
}
}
双向链表
相对于单向链表,双向链表中节点包含了前驱节点引用和后续节点引用,因此查找其前驱节点不必从头节点遍历。
双向链表节点
双向链表示意图
双向链表的插入
双向链表的删除
双向链表的Java实现
1.双向链表中包含前后节点的引用,所以不需要getParent函数,直接通过prior和next引用访问前后节点。
2.插入分为根据指定的节点前插入和后插入。
封装双向链表的节点
public class DLNode {
public int data = 0;
DLNode prior;
DLNode next;
public DLNode() {
}
public DLNode(int data) {
this.data = data;
}
}
双向链表的实现
public class DoubleLinkedList {
private DLNode first;
private DLNode last;
public DoubleLinkedList() {
first = null;
last = null;
}
/**
* 1.增:添加第一个节点
*/
public DLNode addFirst(DLNode node) {
first = node;
last = node;
node.next = null;
node.prior = null;
return node;
}
/**
* 1.增:前插入节点,循环链表的任何节点都有父节点,所以不需要做处理(单个节点的父节点是自身)
*/
public DLNode insBefore(int target, DLNode node) throws Exception {
DLNode pre;
DLNode cur;
cur = this.searchByData(target);
pre = cur.prior;
// 如果pre是null说明在头节点前面插入
if (cur == first) {
node.next = first;
first.prior = node;
node.prior = null;
// 更新first
first = node;
} else {
pre.next = node;
node.prior = pre;
node.next = cur;
cur.prior = node;
}
return node;
}
/**
* 1.增:后插入节点,循环链表的任何节点都有父节点,所以不需要做处理(单个节点的父节点是自身)
*/
public DLNode insAfter(int target, DLNode node) throws Exception {
DLNode aft;
DLNode cur;
cur = this.searchByData(target);
aft = cur.next;
// 当在last节点后插入节点时需要更新last
if (cur == last) {
cur.next = node;
node.prior = cur;
node.next = null;
// 更新last
last = node;
} else {
cur.next = node;
node.prior = cur;
aft.prior = node;
node.next = aft;
}
return node;
}
/**
* 2.删,删除指定的节点,成功返回被删除的节点
*/
public DLNode delByData(int target) throws Exception {
DLNode temp;
if (!this.containsData(target)) {
throw new Exception("该链表不包含此节点,无法删除!");
}
// 链表为空
if (this.isEmpty()) {
throw new Exception("链表为空,无法删除!");
} else if (first == last) {
// 链表只有一个元素的情况
temp = first;
first = null;
last = null;
} else {
// 链表有两个和两个以上的节点
DLNode cur = this.searchByData(target);
temp = cur;
// 删除的为头节点
if (cur == first) {
DLNode aft = cur.next;
aft.prior = null;
first = aft;
// 删除的为尾节点
} else if (cur == last) {
DLNode pre = cur.prior;
pre.next = null;
last = pre;
// 删除其他节点
} else {
DLNode pre = cur.prior;
DLNode aft = cur.next;
pre.next = aft;
aft.prior = pre;
}
}
return temp;
}
/**
* 3.改,修改指定节点的值
*/
public DLNode modifyByValue(int target, int value) throws Exception {
if (!this.containsData(target)) {
throw new Exception("该节点不存在链表中,无法修改!");
}
DLNode cur;
cur = this.searchByData(target);
cur.data = value;
return cur;
}
/**
* 4.查,由data值查找LNode的位置
*/
public DLNode searchByData(int target) throws Exception {
DLNode temp = first;
while (temp != null) {
if (temp.data == target) {
break;
}
temp = temp.next;
}
return temp;
}
/**
* 判断链表是否为空,头节点为空则链表为空。
*/
public boolean isEmpty() {
return first == null;
}
/**
* 链表是否包含node
*/
public boolean containsData(int target) {
boolean flag = false;
DLNode dln = first;
while (dln != null) {
if (dln.data == target) {
flag = true;
break;
}
dln = dln.next;
}
return flag;
}
/**
* 打印链表
*/
public void printLList() {
if (this.isEmpty()) {
System.out.println("链表为空!");
return;
}
DLNode ln = first;
while (ln != null) {
if (ln.next == null) {
System.out.println(ln.data);
break;
}
System.out.print(ln.data + "->");
ln = ln.next;
}
}
单向循环链表
循环链表分为单向循环链表和双向循环链表。
单向循环链表:将单链表的尾节点的next引用指向链表头而不是null,即让单链表的尾和头相连接就构成了单向循环链表。
1.增
添加第一个节点,链表中只含有一个节点时,其后继节点使其自身
在队尾插入
在其他位置插入
单向循环链表Java实现
public class CycleLinkedList {
private LNode first;
private LNode last;
public CycleLinkedList() {
first = null;
last = null;
}
/**
* 1.增,创建第一个节点
*/
public LNode addFirst(LNode node) {
first = node;
last = node;
node.next = first;
return node;
}
/**
* 1.增,插入节点,循环链表的任何节点都有父节点,所以不需要做处理(单个节点的父节点是自身)
*/
public LNode insBefore(int target, LNode node) throws Exception {
LNode pre;
LNode cur;
pre = this.getParent(target);
cur = pre.next;
pre.next = node;
node.next = cur;
// 如果要在第一个节点前插入节点,更新尾节点
if (pre == last) {
last = node;
}
return node;
}
/**
* 1.增,在target后插入node节点,插入成功返回node节点! 同理,循环链表的任意节点都包含后继节点,单个节点的后继节点是其自身
*/
public LNode insAfter(int target, LNode node) throws Exception {
LNode aft;
LNode cur;
cur = this.searchByData(target);
aft = cur.next;
cur.next = node;
node.next = aft;
// 如果在最后一个节点后面插入节点,更新尾节点
if (cur == last) {
last = node;
}
return node;
}
/**
* 2.删,删除指定的节点,成功返回被删除的节点
*/
public LNode delByData(int target) throws Exception {
LNode temp;
if (!this.containsData(target)) {
throw new Exception("该链表不包含此节点,无法删除!");
}
// 链表为空
if (this.isEmpty()) {
throw new Exception("链表为空,无法删除!");
} else if (first == last) {
// 链表只有一个元素的情况
temp = first;
first = null;
last = null;
} else {
// 链表有两个和两个以上的节点
LNode cur;
LNode pre;
cur = this.searchByData(target);
pre = this.getParent(target);
temp = cur;
pre.next = cur.next;
}
return temp;
}
/**
* 4.查,查找target元素对应的父节点,找到就返回父节点,不存在就抛出异常!
*/
public LNode getParent(int target) throws Exception {
if (!this.containsData(target)) {
throw new Exception("链表中不包含该值对应的节点!");
}
// 头节点的父节点为尾节点
if (target == first.data) {
return last;
} else {
LNode pre = first;
LNode cur = first.next;
//因尾节点指向头节点,所以不能再用cur!=null来判断,而应当使用cur!=first
while (cur != first) {
if (cur.data == target) {
break;
}
pre = pre.next;
cur = cur.next;
}
return pre;
}
}
/**
* 打印链表
*/
public void printLList() {
LNode ln = first;
// 打印第一个节点信息
System.out.print(ln.data);
ln = ln.next;
//打印的判断条件也需要调整
while (ln != first) {
System.out.print("->" + ln.data);
ln = ln.next;
}
System.out.println();
}
}
双向循环链表
双向循环链表相对于双向链表,其尾节点的后继节点next不再指向null,而是指向头节点即尾节点的后继节点为头节点;其头节点的前驱节点不再指向null,而是指向尾节点,头节点的前驱节点为尾节点。
Java实现
相对于双向链表需要修改
1.添加第一个节点(add)
2.前插(insBefore),后插操作(insAfter)
3.删除操作(delBydata)
4.涉及变量链表的操作:查找target对应的节点(searchByData),链表是否包含节点(contains),打印链表(printLList)
public class DoubleCycleLinkedList {
private DLNode first;
private DLNode last;
public DoubleCycleLinkedList() {
first = null;
last = null;
}
/**
* 1.增:添加第一个节点(头节点经过创建不在改变,修改尾节点)
*/
public DLNode addFirst(DLNode node) {
first = node;
last = node;
node.next = node;
node.prior = node;
return node;
}
/**
* 1.增:前插入节点,循环链表的任何节点都有父节点,所以不需要做处理(单个节点的父节点是自身)
*/
public DLNode insBefore(int target, DLNode node) throws Exception {
DLNode pre;
DLNode cur;
cur = this.searchByData(target);
pre = cur.prior;
// 插入节点
node.next = first;
first.prior = node;
node.prior = pre;
pre.next = node;
// 如果cur是头节点,那么插入后需要更新尾节点
if (cur == first) {
last = node;
}
return node;
}
/**
* 1.增:后插入节点,循环链表的任何节点都有父节点,所以不需要做处理(单个节点的父节点是自身)
*/
public DLNode insAfter(int target, DLNode node) throws Exception {
DLNode aft;
DLNode cur;
cur = this.searchByData(target);
aft = cur.next;
// 插入节点
cur.next = node;
node.prior = cur;
node.next = aft;
aft.prior = node;
// 当在last节点后插入节点时需要更新last
if (cur == last) {
last = node;
}
return node;
}
/**
* 2.删,删除指定的节点,成功返回被删除的节点
*/
public DLNode delByData(int target) throws Exception {
DLNode temp;
if (!this.containsData(target)) {
throw new Exception("该链表不包含此节点,无法删除!");
}
// 链表为空
if (this.isEmpty()) {
throw new Exception("链表为空,无法删除!");
} else if (first == last) {
// 链表只有一个元素的情况
temp = first;
first = null;
last = null;
} else {
// 链表有两个和两个以上的节点
DLNode cur = this.searchByData(target);
temp = cur;
DLNode pre = cur.prior;
DLNode aft = cur.next;
// 删除节点
pre.next = aft;
aft.prior = pre;
// 删除的为头节点,更新头节点为下一个节点
if (cur == first) {
first = aft;
// 删除的为尾节点,更新尾节点为前一个节点
} else if (cur == last) {
last = pre;
}
}
return temp;
}
/**
* 4.查,由data值查找LNode的位置
*/
public DLNode searchByData(int target) throws Exception {
DLNode temp = first;
while (temp != null) {
if (temp.data == target) {
break;
}
temp = temp.next;
}
return temp;
}
/**
* 链表是否包含node
*/
public boolean containsData(int target) {
boolean flag = false;
DLNode dln = first;
// 首先判断第一个元素是否为target
if (first.data == target) {
flag = true;
} else {
// 如果第一个元素不是target,从第二个元素开始遍历链表,循环条件改变为dln!=first
while (dln != first) {
if (dln.data == target) {
flag = true;
break;
}
dln = dln.next;
}
}
return flag;
}
/**
* 打印链表
*/
public void printLList() {
if (this.isEmpty()) {
System.out.println("链表为空!");
return;
}
DLNode ln = first;
System.out.print(ln.data);
ln = first.next;
while (ln != first) {
System.out.print("->" + ln.data);
ln = ln.next;
}
System.out.println();
}
---------------------------------------------------------------------------------------------------------------------------------------------参考资料:
数据结构 严蔚敏 吴伟民
https://www.jianshu.com/p/1cbd7e7414b7
https://www.jianshu.com/p/49f1f6c1cd3d
https://blog.csdn.net/LYR1994
https://blog.csdn.net/qq_25343557/article/details/83661240
https://blog.csdn.net/fanyun_01/article/details/79831877
https://blog.csdn.net/sinat_39061823/article/details/74935755
https://blog.csdn.net/wyqwilliam/article/details/82719058
https://blog.csdn.net/wangzhen209/article/details/8180462
( 注:部分资料参考网络,如有侵权或错误请在留言与我联系,欢迎关注我的微信公众号“搬砖小张”,一起学习,一起进步)