在线excel表格助力企业高效管理与数据分析的未来趋势
669
2022-05-29
学习目标
数组的结构以及各种操作
单链表、双链表、循环链表的实现及操作
栈、队列的实现以及各种基本操作
二叉树的结构与实现
遍历二叉树
数据结构介绍:
软件(software)是计算机系统中与硬件(hardware)相互依存的一部分,它包括程序(program)、相关数据(data)及其说明文档(document)。程序来操作数据,如果数据组织的不好就会造成效率问题,甚至造成程序不能运行。因此数据结构就诞生了。数据结构从概念上讲有数据的逻辑结构和数据的物理结构(也叫存储结构)。数据的逻辑结构就是数据表面上的关系。例如:如果每个数据节点,除了首节点和尾节点外,每个节点都只有一个直接前驱和一个直接后继,那么就是个线性表。该类型常见的结构有:数组,队列,堆栈,链表等。如果除了首节点外,每个节点都只有一个直接前驱和多个直接后继,那么该结构就是一个树型结构。如果每个节点都有多个直接前驱和多个直接后继,那么就是图形结构。数据的物理结构只有两个:一个是顺序存储,一个是链接式存储。下面将使用JAVA语言来实现各个数据结构以及算法。
数组
通过索引(下标)来访问数据元素。索引是一种随机访问操作。随机访问的特点就是不需要从数组的第一个元素开始访问要查找的元素,而是可以直接访问查找的元素,在随机访问中,查找第一个元素和查找最后一个元素所需时间是一样的。数组中的数据是保存在一个地址连续的内存中,它们是紧密相邻的。当查找第i个元素时,计算机是将数组的基地址加上数据的偏移量的值来确定第i个元素的。基地址指第一个元素的地址,偏移量等于第i个元素的索引乘以一个常量,这个常量是数组中一个元素所需的内存单元数目(字节)。
在java和c++中,所有的数组都是动态数组,即数组的大小可以在运行的时候才确定。
比如:
void m1(int i){
int[] arra = new int[i];
}
逻辑大小和物理大小
物理大小指数组的容量大小,而逻辑大小则指数组中实际已存储的数据元素个数。Java中通过数组的length属性来得到数组大小
如果数组的逻辑大小等于物理大小,则代表数组已满;如果数组的逻辑大小为0,则代表数组为空;在数组已满的情况下,数组的最后一个元素的索引(下标)值为逻辑大小减一。
链表
由多个节点(对象)组成,其中每个节点在内存中是散乱存放的,即存放在一个叫做对象堆的不连续地址内存中。其中每个节点(除了尾节点外)都有一个特定的引用下一个节点的变量,从而实现一个完整链表。
链表中的每个元素都称为节点,每个节点包含数据内容和引用后一个节点的变量(通常叫做指针)。
数组中的元素是存储在地址连续的内存中,而链表中的元素则是散乱存放在内存中的。
不能通过索引来访问链表,查找链表中的数据则必须从链表的一端开始沿着链一直移动直到找到查询的数据为止。
由于链表的不连续地址内存机制,在往链表插入和删除数据时,不会象数组那样移动其他数据项,而是在空闲内存中为新的节点对象分配空间,然后将该节点和链表中特定位置的节点链接起来。
链表类型
单链表
单链表节点包含一个数据内容和指向后一个节点的引用。其中,单链表的最后一个节点的指针指向null,第一个节点用一个特殊的头节点来指向。
单链表只能以向后的方式访问链表节点!
单链表的实现及各种操作:
见例题。
双向链表
双向链表节点不但包含单链表节点有的内容之外,还包含一个指向前一个节点的引用,通过向前指针和向后指针来实现双向访问链表节点!
同样,双向链表也有头部和尾部,有两个特殊链表指针,分别指向第一个节点和最后一个节点。
双向链表和单链表的区别在于,双向链表可以前后访问节点!
双向链表的实现和操作:
见例题。
循环链表
最后一个节点的指针指向第一个节点,其余和单链表类似。
实现和操作:
见例题。
三、栈(stack)
一个仅在一端访问的线性集合,这端叫做top(栈顶)。
遵循后进先出的协议(LIFO),即最后加入栈的数据会最先被取出。
push----下推数据到栈
pop----从栈顶取出最后一个被推进来的数据
栈的应用:
中缀表达式到后缀表达式的转换,以及对后缀表达式的求值
回溯算法
方法调用
文本编辑器中的撤销功能
web浏览器的链接历史信息的保留
中缀表达式到后缀表达式的转换:
从左至右读取表达式
若读取的是操作数,则添加到后缀表达式中
若读取的是运算符:
如果运算符为"(",则直接推入栈中
如果运算符为")",则取出栈中的右运算符,添加到后缀表达式中,直到取出左括号为止。
如果运算符为非括号运算符,则与栈顶的运算符做比较,如果比栈顶运算符优先级高或相等,则直接推入栈,否则取出栈中运算符,添加到后缀表达式中。
当表达式读取完成,栈中还有运算符时,则依序取出栈中运算符,并分别追到后缀表达式中,直到栈为空。
后缀表达式的求值:
从左至右读取表达式:
若读取的是操作数,则将其推入栈中
若是运算符,则从栈中取出两个操作数进行计算,并将结果推入栈中。
重复以上步骤,直到表达式读取完毕。
队列:
线性集合,只允许在表的一端进行插入,即队尾(rear),删除则在另一端,即队头(front)。支持先进先出(FIFO)。
队列应用:
CPU访问
磁盘访问
打印机访问
树:
由一个或多个节点组成的有限集合。每一颗树必须有一个特定节点,叫做根节点。根节点下可以有零个以上的子节点。而且各子节点也可以为子树,拥有自己的子节点。
若一棵树中的节点最多可以有n个节点,则称该树为n元树。
树的相关名称
根节点(root node):树中没有父节点的节点即为根节点
叶节点(leaf node):没有子节点的节点
非叶节点:叶节点以外的节点
父节点(parent)和子节点(child)
兄弟节点(sibling):同一个父节点的节点
分支度(degree):每个节点所拥有的子节点个数,树中的最大的分支度值即为该树的分支度
阶层(level):根节点的阶层为1,其子节点为2,依次类推
高度和深度:树的最大阶层值即为树的高度或深度
祖先(ancestor):由某子节点到根节点的路径上的所有节点,均称为该节点的祖先
二叉树
树的一种,节点最多只能有两个节点
由有限个节点组成的集合,集合可以为空
根节点下可有两个子树,为左子树和右子树
二叉树与树的区别:
二叉树可以为空,而树不可以(至少要有根节点)
二叉树的子树有顺序关系
二叉树的分支度必须为0、1或2,而树的分支度可以大于2
二叉树类型:
左(右)歪斜树
所有节点的左子树均不存在,则此二叉树为右歪斜树
反之,则称之为左歪斜树。
满二叉树
所有叶节点均在同一阶层,其他非叶节点的分支度为2
若此树的高度为n,则该树的节点数为2^n - 1.
完全二叉树
一个二叉树除掉最大阶层后为满二叉树,且最大阶层的节点均向左靠齐。
二叉树的节点插入规则:
均转换成满二叉树的形式来插入节点数据。
对各阶层的节点由低阶层到高阶层,从左至右,由1开始编号,再根据编号将节点数据存入相应索引编号的数组(链表)中
如果某编号没有节点存在,则数组对应位置没有值存入。
插入的第一个元素为根节点
满足左小右大的二叉查找树规则
提问:依次输入数据6,3,8,5,2,9,4,7建立一个二叉树,请描述该二叉树节点的排列次序。
二叉树的三种表示法:
数组表示法
见例题。
优点:
查找容易且每个节点占用空间不大
缺点:
当二叉树的深度和节点数的比例偏高时,会造成空间浪费
数据的插入和删除涉及到移动大量数据的问题
节点数组表示法
包含三个数组:
一个数组存放节点数据内容
一个数组存放左子节点在数组中的下标
一个数组存放右子节点在数组中的下标
见例题
改良后的数组表示法,在插入和删除方面需移动的数据大大减少
链表表示法
在修改二叉树方面效率比数组实现高。
二叉树的遍历:
前序遍历(preorder traversal)
先遍历中间节点,再遍历左子树,最后遍历右子树
伪码表示:
If 指向根节点的指针 == null
Then 此为空树,遍历结束
Else
(1) 处理当前节点
(2) 往左走,递归处理preorder(root ' left)
(3) 往右走,递归处理preorder(root 'right)
中序遍历(inorder traversal)
先遍历左子树,再遍历中间节点,最后遍历右子树
伪码表示:
If 指向根节点的指针 == null
Then 此为空树,遍历结束
Else
(1)往左走,递归处理preorder(root ' left)
(2)处理当前节点
(3)往右走,递归处理preorder(root 'right)
后序遍历(postorder traversal)
先遍历左子树,再遍历右子树,最后遍历中间节点
伪码表示:
二叉树的查找:
先将二叉树转换成二叉查找树,即左小右大,接着可以采用二分查找方式来查找。
对二叉查找树的查找效率高于对非二叉查找树的查找
见例题。
二叉树的删除:
见例题。
分为几种情况:
1. 删除节点既无左子树也无右子树
根节点
非根节点
2. 删除节点有左子树,无右子树
3. 删除节点有右子树,无左子树
4. 删除节点既有左子树,也有右子树
实例分析
数组
数组是常用的数据结构。几乎每种编程语言里面都有该结构。数组的优点是快速的插入数据,如果下标(索引值)知道,可以很快地存取数据。数组的缺点是查找数据慢,删除数据慢,固定大小。
请看下例:
public class ArrayApp {
public static void main(String[] args) {
int[] arr;
arr = new int[100];
int nElems = 0; // 记录元素的个数
int j; // 循环变量
int searchKey; // 要查找的数据
// 插入10个元素
arr[0] = 7;
arr[1] = 2;
arr[2] = 4;
arr[3] = 5;
arr[4] = 9;
arr[5] = 3;
arr[6] = 1;
arr[7] = 0;
arr[8] = 6;
arr[9] = 8;
nElems = 10;
System.out.println("---------遍历-----------");
for (j = 0; j < nElems; j++)
// 打印所有的数据
System.out.print(arr[j] + " ");
System.out.println("");
System.out.println("-------查找5------------");
searchKey = 5;
for (j = 0; j < nElems; j++)
if (arr[j] == searchKey) // 如果找到跳出循环
break;
if (j == nElems) // 如果是通过break跳出循环,则n值不等于nElems
System.out.println("找不到 " + searchKey); // yes
else
System.out.println("找到 " + searchKey); // no
System.out.println("------删除6------------");
searchKey = 6; // 删除 6
for (j = 0; j < nElems; j++)
// 找到6的位置
if (arr[j] == searchKey)
break;
for (int k = j; k < nElems; k++)
// 6位置后面的数据依次往前顺移
arr[k] = arr[k + 1];
nElems--; // 个数减一
System.out.println("删除成功");
System.out.println("-----遍历---------------");
for (j = 0; j < nElems; j++)
// 遍历所有数据
System.out.print(arr[j] + " ");
System.out.println("");
} // end main()
} // end class ArrayApp
上面的代码类似于C语言的编程风格。在面向对象的编程思维里,应该按模块化的设计方式,用类来描述一个对象的信息,定义方法来封装该对象的功能,定义属性来区别不同的对象。请看下例:
public class TestMyArray {
public static void main(String[] args) {
MyArray ma = new MyArray();
ma.add(3);
ma.add(1);
ma.add(9);
ma.add(5);
ma.add(7);
System.out.println(ma);
// ------------------------------------------------
System.out.println("---------------------");
if (ma.find(5) != -1)
System.out.println("找到5");
else
System.out.println("没找到5");
// -------------------------------------------------
System.out.println("---------------------");
if (ma.delete(5))
System.out.println("删除成功");
else
System.out.println("删除失败");
// -----------------------------------------------
System.out.println("---------------------");
System.out.println(ma);
}// end main()
}// end class TestMyArray
class MyArray {
int[] arr; // 声明int类型的数组的引用
int nElements; // 记录数组里面元素的个数
int size; // 数组的大小
public MyArray() {
// 默认情况下,该数组对象的大小为10
this(10);
}
public MyArray(int size) {
nElements = 0;
this.size = size;
arr = new int[size];
}
// 增加的方法
public boolean add(int val) {
if (nElements < size)// 判断是否数组已经满了
{
arr[nElements++] = val; // nElements 既是下标,又记录元素个数
return true;
} else {
return false;
}// end if
}// end add(int val)
public int get(int index)// 获取的方法
{
return arr[index];
}
public int find(int key) // 查找的方法,如果找到返回改值所在的下标,否则返回-1
{
int i = 0;
for (; i < nElements; i++)
// 循环查找
if (arr[i] == key) // 如果找到跳出循环
break;
if (i == nElements) // 如果i==nElements表示不是通过break跳出循环的,找不到!
return -1; // -1表示找不到
else
return i; // i是找到值所在的下标
}
public boolean delete(int key) // 删除的方法,true 表示删除成功,false表示失败
{
int k = find(key); // 首先先查找要删除的值存在不存在
if (k == -1) {
return false;
} else {
for (int i = k; i < nElements; i++)
arr[i] = arr[i + 1];
nElements--;
return true;
}// end if
}// end delete(int key)
public String toString() {
StringBuffer sb = new StringBuffer();
for (int i = 0; i < nElements; i++) {
if (i != nElements - 1)
sb.append(arr[i] + ",");
else
sb.append(arr[i]);
}// end for
return sb.toString();
}
public int getSize() // 得到该数组的大小
{
return nElements;
}// end getSize()
};// end MyClass
运行结果
堆栈和队列的实现
堆栈和队列都是线性表结构。只不过堆栈的逻辑数据特点是先进后出(FILO),而队列的逻辑数据特点是先进先出(FIFO)。我们先用数组来存放这两种数据结构,也就是线性的存储结构。
请看下例:
public class StackT {
private int maxSize; // 堆栈的大小
private int[] stackArray;
private int top; // 堆栈顶部指针
public StackT(int s) {
maxSize = s; // 初始化数组大小
stackArray = new int[maxSize]; // 初始化一个数组
top = -1;
}
public void push(int j) // 压栈
{
if (!isFull()) // 先判断是否为空
stackArray[++top] = j;
else
return;
}
public int pop()// 出栈
{
return stackArray[top--];
}
public int peek()// 得到栈顶的数据而不是出栈
{
return stackArray[top];
}
public boolean isEmpty()// 判断栈是否为空
{
return (top == -1);
}
public boolean isFull() // 判断是否栈满
{
return (top == maxSize - 1);
}
public String toString()// 按照堆栈的特点打印所有数据
{
StringBuffer sb = new StringBuffer();
for (int i = top; i >= 0; i--)
sb.append("" + stackArray[i] + "\n");
return sb.toString();
}
} // end class StackX
class StackApp {
public static void main(String[] args) {
StackT theStack = new StackT(10); // 初始化一个堆栈
theStack.push(20);
theStack.push(40);
theStack.push(60);
theStack.push(80);
System.out.println(theStack);
System.out.println("");
} // end main()
} // end class StackApp
运行的结果如图:
队列的实现(循环队列):
public class Queue {
private int maxSize; // 表示队列的大小
private int[] queArr; // 用数组来存放有队列的数据
private int front; // 取数据的下标
private int rear; // 存数据的下标
private int nItems; // 记录存放的数据个数
public Queue(int s) {
maxSize = s;
queArr = new int[maxSize];
front = 0;
rear = -1;
nItems = 0;
}
public void insert(int j) // 增加数据的方法
{
if (isFull())
return;
// 如果下标到达数组顶部的话,让rear指向数组的第一个位置之前
if (rear == maxSize - 1)
rear = -1;
queArr[++rear] = j; // increment rear and insert
nItems++; // one more item
}
public int remove() // 删除元素
{
int temp = queArr[front++];
// 如果下标到达数组顶部的话,让front指向数组的第一个位置
if (front == maxSize)
front = 0;
nItems--;
return temp;
}
public int peekFront()// 只是返回最前面那个元素的值,并不删除
{
return queArr[front];
}
public boolean isEmpty() {
return (nItems == 0);
}
public boolean isFull() {
return (nItems == maxSize);
}
public int size() {
return nItems;
}
} // end class Queue
class QueueApp {
public static void main(String[] args) {
Queue theQueue = new Queue(5);
theQueue.insert(10); // 插入4个数据
theQueue.insert(20);
theQueue.insert(30);
theQueue.insert(40);
theQueue.remove(); // 删除(10, 20, 30)
theQueue.remove();
theQueue.remove();
theQueue.insert(50); // 再插入4个数据
theQueue.insert(60);
theQueue.insert(70);
theQueue.insert(80);
while (!theQueue.isEmpty())
// 取出所有数据
System.out.println(theQueue.remove());
} // end main()
} // end class QueueApp
运行的结果如图:
单链表的实现
链表也是一种线性表。它主要体现在物理结构上,节点跟节点之间是通过连接的方式来组织所有数据的。单链表是上一个节点存放了下一个节点在内存里面的地址。而下一个节点并没有存储上一个节点的地址。
代码的实现:
/**
* @see 链表类,实现了遍历、反转、插入、删除
*/
public class LinkedOfSample {
// 定义链表的最大长度
private int MAXLENGTH = 20;
// 长度计数
private int count;
// 定义引用第一个节点的start对象
Node start = null;
// 定义用来指向前一个节点变量及指向当前节点变量
Node prev, curr;
/**
* @see 判断长度是否达到最大值
* @return boolean
*/
public boolean count() {
if (count > MAXLENGTH) {
return false;
} else {
return true;
}
}
/**
* @see 添加新的节点至链表的末端
* @param dataID(String)
* --新节点的数据部分
* @param dataName(String)
* --新节点的数据部分
*/
public void addNodeToTail(String dataID, String dataName) {
if (count()) {
// 创建新的链表
if (start == null) // start为NULL代表链表中无节点
{
// 创建新节点并让start指向新接点
start = new Node(dataID, dataName, start);
}
// 往已有节点链表中添加新节点
else {
// 指向链表的开始处(即第一个节点)
prev = curr = start;
// 遍历链表
while (curr != null) {
prev = curr;
curr = curr.next;
}
/*
* 创建新节点,并将新节点的NEXT指向NULL ,做为链表的最后一个节点添加
*/
Node n = new Node(dataID, dataName, curr);
// 将原链表最后一个节点的NEXT指向新节点
prev.next = n;
}
// 添加计数
count++;
} else {
System.out.println("链表已满!");
}
}
/**
* @see 从链表的中间加入节点
* @param keyID(String)
* --用户指定要插入的链表位置
* @param dataID(String)
* --新节点的数据部分
* @param dataName(String)
* --新节点的数据部分
*/
public void insert(String keyID, String dataID, String dataName) {
if (count()) {
// 指向链表的开始处(即第一个节点)
curr = prev = start;
while (curr != null) {
// 如果找到匹配值就停止遍历,curr定位在插入位置
if (curr.cust.getNumID() == keyID) {
break;
}
prev = curr;
curr = curr.next;
}
// 新节点内next变量指向新节点插入位置的下个节点
Node n = new Node(dataID, dataName, curr);
// 上一个节点的next变量指向新节点
prev.next = n;
// 长度加一
count++;
} else {
System.out.println("链表已满!");
}
}
/**
* @see 添加新节点至链表的头部
* @param cust
*/
public void addNodeToHeader(Customer cust) {
if (count()) {
// 得到第一个节点的位置
curr = start;
/*
* 实现了节点的添加 1.创建新节点 2.将start指向新节点 3.将新节点的next变量指向了原链表的第一个节点
*/
start = new Node(cust, curr);
count++;
} else {
System.out.println("链表已满!");
}
}
/**
* @see 遍历整个链表并显示
*
*/
public void traverse() {
if (count == 0) {
System.out.println("链表为空!");
return;
}
curr = start;
while (curr != null) {
// 显示链表中的节点数据
curr.cust.dis();
// 让curr读取下一个节点
curr = curr.next;
System.out.println();
}
}
/**
* @see 反转整个链表
*/
public void reverse() {
// 只有一个节点则不需反转
if (count == 1) {
return;
}
/*
* curr,temp指向当前节点, prev指向前一个节点
*/
Node temp;
prev = curr = temp = start;
while (temp != null) {
prev = curr;
curr = temp;
// 处于链表头部时的反转
if (prev == start) {
// 让curr指向下一个节点
curr = curr.next;
// 将链表头节点的next指向空
prev.next = null;
}
// 将下个节点的引用值保存在temp变量中
temp = curr.next;
// 将下个节点的next指向上个节点(反转)
curr.next = prev;
}
// 最后节点变为第一个节点,完成反转
start = curr;
}
/**
* @see 删除指定节点
* @param ID
*/
public void deleteNodeByCust(String ID) {
prev = curr = start;
// 找到删除节点的位置
while (curr != null) {
if (curr.cust.getNumID() == ID) {
break;
}
prev = curr;
curr = curr.next;
}
if (curr == null) {
System.out.println("没有您要删除的数据!");
return;
}
if (prev == curr) {
start = start.next;
} else {
// 将节点从链表中解链
prev.next = curr.next;
}
System.out.println("删除后的队列内容:");
traverse();
}
/**
* @param args
*/
public static void main(String[] args) {
// TODO 自动生成方法存根
LinkedOfSample list = new LinkedOfSample();
String[][] strTmp = { { "1", "2", "3" }, { "001", "002", "003" } };
for (int i = 0; i < 3; i++) {
// 在链表尾部线序添加节点
list.addNodeToTail(strTmp[0][i], strTmp[1][i]);
}
// 从指定位置2插入值4
list.insert("2", "4", "004");
list.addNodeToHeader(new Customer("5", "005"));
list.addNodeToTail("6", "006");
list.traverse();
// 反转链表
// list.reverse();
//
// System.out.println("链表反转结果:");
//
// list.traverse();
// 删除指定节点
list.deleteNodeByCust("5");
}
}
/**
* @see 组成链表的节点类
* @author Alan
*/
class Node {
// 节点数据部分
Customer cust;
// 节点的next变量,链表中的链线
Node next;
public Node(String _numID, String _strName, Node n) {
cust = new Customer(_numID, _strName);
next = n;
}
public Node(Customer _cust, Node n) {
cust = _cust;
next = n;
}
}
/**
* @see 节点的客户数据类
* @author Alan
*/
class Customer {
private String numID;
private String strName;
public Customer(String _numID, String _strName) {
numID = _numID;
strName = _strName;
}
public String getNumID() {
return numID;
}
public String getStrName() {
return strName;
}
public void dis() {
System.out.println("Customer details are : ");
System.out.println("Customer ID: " + numID);
System.out.println("Customer Name: " + strName);
}
}
运行结果:
双链表的实现
上一章里介绍了单链表,单链表是上一个节点存放了下一个节点在内存里面的地址。而下一个节点并没有存储上一个节点的地址。双链表是除了首节点和尾节点之外,每个节点都存放了上一个节点和下一个节点在内存里的地址。换句话说,双链表里每个节点有三个属性构成:数据,上一个节点的引用和下一个节点的引用。要想实现双链表,首先要创建一个节点类,包含上面的三个属性:
请看下例:
public class Node {
public Node next;// 引用下一个节点
public Node previous;// 引用上一个节点
private int data;// 节点中存放的数据
public Node(int data) {
this.data = data;
}
public String toString()// 覆盖该方法,用来打印该节点
{
return data + "";
}
public int getData() {
return data;
}
public void setData(int data) {
this.data = data;
}
public boolean equals(Object other)// 进行两个节点的比较
{
Node temp = null;
if (other instanceof Node)// 判断是否是Node类型的
{
temp = (Node) other;
if (temp.getData() == this.getData())// 进行数据的比较
return true;
else
return false;
} else// 如果不是Node类型直接返回false
{
return false;
}
}
}
// 下一步就是把节点联系起来组成一个双链表
class DoubleLink {
private Node first;
private Node end;
public void addFirst(int data) {
Node node = new Node(data);
if (first != null) {
node.next = first;
first.previous = node;
first = node;
} else {
first = node;
end = node;
}
}
public void addEnd(int data) {
Node node = new Node(data);
if (end != null) {
end.next = node;
node.previous = end;
end = node;
} else {
first = node;
end = node;
}
}
public Node find(int data)// data是要找的数据
{
Node temp = new Node(data);// 以该数据创建一个节点进行比较
Node f = first; // 从头节点开始找
while (f != null) {
if (f.equals(temp))
break;
else
f = f.next;
}
return f;
}
public void delete(int data) {
Node node = find(data); // 首先查找容器中有无该数据
if (node != null) // 如果找到要删的数据,进行指针的移位,从而删除该数据
{
node.previous.next = node.next;
node.next.previous = node.previous;
}
}
public void update(int ydata, int mdata)// 更新数据
{
Node node = find(ydata);// 查找数据
node.setData(mdata);// 修改数据
}
public String toString() {
// 不要直接使用String类,存在效率问题
StringBuffer sb = new StringBuffer();
Node temp = first;
while (temp != null) {
if (temp == end)
sb.append("[" + temp.getData() + "]");
else
sb.append("[" + temp.getData() + "],");
temp = temp.next;
}
return sb.toString();
}
}
// 下面写个类来测试运行的结果:
public class TestDL {
public static void main(String[] args) {
DoubleLink dl = new DoubleLink();
dl.addFirst(30);
dl.addFirst(40);
dl.addFirst(50);
dl.addFirst(60);
dl.addEnd(70);
dl.addEnd(80);
dl.addEnd(90);
dl.delete(50);
System.out.println("result:");
System.out.println(dl);
}
}
运行结果如图
二叉树的结构与实现
树型结构是用来存取数据的效率比较好的一种数据结构,增,删,改效率都比前面介绍的数据结构要高。缺点就是实现起来比较复杂。下面以二叉树为例子,来说明数型结构的特点:
请看下例:
二叉树的实现:
class JD {
int data;// 数据
JD left; // 左儿子
JD right;// 右儿子
public JD(int data) {
this.data = data;
}
public String toString() {
return data + "";
}
};
// 该类实现了增,删,改,查等特性
class Tree{
JD root;
JD parrent;
boolean b;
public boolean add(int d)// 增加数据的方法
{
JD jd = new JD(d);
if (root == null) // 如果根节点为空,那么把新节点加给根节点
root = jd;
else {
JD current = root;
while (current != null)// 是找到一个位置加新节点
{
if (d == current.data)// 如果已经存在,则直接返回false 表示加失败
return false;
else if (d > current.data) {// 如果该值大于当前节点,那么应该往右边找
parrent = current; // 记录要加新节点的父节点
b = true; // 记录是左边还是右边,
current = current.right;// current.right=current
} else if (d < current.data) {
parrent = current;
b = false;
current = current.left;// current.left=current
}
}// end while
if (b)// 如果是右儿子为空 ,就加父节点的右边
parrent.right = jd;
else
parrent.left = jd;
}
return true;
}
public JD find(int d)// 查询的方法
{
JD current = root;
while (current != null) {
if (current.data == d)
return current;
else {
parrent = current;// 记录找到节点的父节点,以方便删除操作
if (d > current.data) {
current = current.right;
b = true;
} else if (d < current.data) {
current = current.left;
b = false;
}
}
}// end while
return current;
}
public boolean delete(int d) {
// 删除数据的方法
JD current = find(d);// 先找到要删除的节点
if (current == null)
return false;
else if (current.left == null && current.right == null) // 如果要删除的节点是//页节点
{
if (current == root)// 如果是根节点(也是页节点),直接让根节点==null
root = null;
else if (b)
parrent.right = null;
else
parrent.left = null;
} else if (current.left == null)// 如果删除节点只有右节点
{
if (b)
parrent.right = current.right;
else
parrent.left = current.right;
} else if (current.right == null)// 如果删除节点只有左节点
{
if (b)// 该变量记录了要删除的节点,在其父节点的左边还是右边
parrent.right = current.left;
else
parrent.left = current.left;
} else {
JD temp = fenlie(current); // 分裂节点
if (b)
parrent.right = temp;
else
parrent.left = temp;
}
return true;
}
public JD fenlie(JD c) {
JD temp = c.right;
JD p = temp;// 用来记录要删除节点右儿子那边的最小节点
JD pp = temp;// 用来记录要删除节点右儿子那边的最小节点的父节点
while (temp != null)// 找到要删除节点右儿子那边的最小节点
{
pp = p;
p = temp;
temp = temp.left;
}
if (pp == p)// 如果删除节点的右儿子节点没有左儿子
{
pp.left = c.left;// 把删除节点左儿子部分加到删除节点的右儿子的左边
return pp;
} else {
pp.left = p.right;// 把找到的节点的右儿子部分加到该节点父节点的左边
p.left = c.left;// 把删除节点的左儿子加到分裂节点的左边
p.right = c.right;// 把删除节点的右儿子加到分裂节点的右边
return p;
}
}
public boolean modify(int s, int m)// 修改数据的方法=先删除后增加,这样还是有//顺序的
{
delete(s);
return add(m);
}
public void print(JD jd)// 递归中序遍历该有序二叉树
{
if (jd != null) {
print(jd.left);
System.out.print(jd + " ");
print(jd.right);
}
}
public void p() {
print(root);
}
}
class TestTree {
public static void main(String[] args) {
Tree t = new Tree();
t.add(5);
t.add(7);
t.add(3);
t.add(9);
t.add(1);
t.add(8);
t.add(13);
t.add(4);
t.p();
System.out.println("\n" + "-------------改变----------");
t.modify(8, 18);// 把接点值为8的接点,把该值改为18
t.delete(9);// 删除接点值为9的接点
t.p();
}
}
运行的结果如图
(图 10)
遍历二叉树
遍历二叉树有很多种方式。大的范围分两种:横向遍历和纵向遍历(深度遍历)。纵向遍历又分为三种:前序,中序,后序。先遍历根接点是前序遍历,根接点中间遍历是中序遍历,根接点在后面遍历就是后续。如下图二叉树的前序,中序,后序遍历结果:
前序遍历结果为:
57,42,35,23,9,24,48,45,43,51,70,62,61,68,66,69,
72,78,77,91
中序遍历结果为:
9,23,24,35,42,43,45,48,51,57,61,62,66,68,69,70,
72,77,78,91
后序遍历结果为:
9,24,23,35,43,45,51,48,42,61,66,69,68,62,77,91,
78,72,70,57
也就是说该二叉树是按中序有序遍历的二叉树。
package com.itjob;
public class TreeNode {
TreeNode leftNode;
int data;
TreeNode rightNode;
public TreeNode(int nodeData) {
data = nodeData;
}
public synchronized void insert(int insertValue) {
if (insertValue < data) {
if (leftNode == null)
leftNode = new TreeNode(insertValue);
else
leftNode.insert(insertValue);
}else if (insertValue > data) {
if (rightNode == null)
rightNode = new TreeNode(insertValue);
else
rightNode.insert(insertValue);
}
}
}
package com.itjob;
public class MyTree {
private TreeNode root;
public MyTree() {
root = null;
}
// 插入节点到树
public synchronized void insertNode(int insertValue) {
if (root == null)
root = new TreeNode(insertValue);
else
root.insert(insertValue);
}
// 开始先序遍历
public synchronized void preorderTraversal() {
preorderHelper(root);
}
// 执行先序遍历的方法
private void preorderHelper(TreeNode node) {
if (node == null)
return;
System.out.print(node.data + " ");
preorderHelper(node.leftNode);
preorderHelper(node.rightNode);
}
// 开始中序遍历
public synchronized void inorderTraversal() {
inorderHelper(root);
}
// 执行中序遍历的方法
private void inorderHelper(TreeNode node) {
if (node == null)
return;
inorderHelper(node.leftNode);
System.out.print(node.data + " ");
inorderHelper(node.rightNode);
}
// 开始后序遍历
public synchronized void postorderTraversal() {
postorderHelper(root);
}
// 执行后序遍历的方法
private void postorderHelper(TreeNode node) {
if (node == null)
return;
postorderHelper(node.leftNode);
postorderHelper(node.rightNode);
System.out.print(node.data + " ");
}
}
package com.itjob;
public class TreeTest {
public static void main(String args[]) {
MyTree tree = new MyTree();
int value;
System.out.println("Inserting the following values: ");
// 插入10个0-99的随机数
for (int i = 1; i <= 10; i++) {
value = (int) (Math.random() * 100);
System.out.print(value + " ");
tree.insertNode(value);
}
System.out.println("先序遍历");
tree.preorderTraversal();
System.out.println("中序遍历");
tree.inorderTraversal();
System.out.println("后序遍历");
tree.postorderTraversal();
System.out.println();
}
}
内容总结
使用数组存储并操作数据;
使用链表存储并操作数据;
使用树存储并操作数据
独立实践
使用递归算法写一个二叉树的创建与遍历。
使用单链表创建一个栈和队列数据结构。
数据结构
版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。