數據結構與算法(棧、隊列、鏈表)


棧是一種特殊的線性表,僅能夠在棧頂進行操作,有着先進后出的特性

 

 

我們先定義一個簡單的 Stack 類

function Stack(){
  var items = []; // 使用數組存儲數據
}

 

棧有以下幾個方法:

  • push:添加一個元素到棧頂
  • pop:彈出棧頂元素
  • top:返回棧頂元素(不是彈出)
  • isEmpty:判斷棧是否為空
  • size:返回棧里元素的個數
  • clear:清空棧
function Stack(){
  var items = []; // 使用數組存儲數據

  // 從棧頂添加元素,也叫壓棧
  this.push = function(item){
    items.push(item)
  }

  // 彈出棧頂元素
  this.pop = function(){
    return items.pop()
  }

  // 返回棧頂元素
  this.top = function(){
    return items[items.length - 1]
  }

  // 判斷棧是否為空
  this.isEmpty = function(){
    return items.length === 0
  }

  // 返回棧的大小
  this.size = function(){
    return items.length
  }

  // 清空棧
  this.clear = function(){
    items = []
  }
}

 

練習1:合法括號

下面的字符串中包含小括號,請編寫一個函數判斷字符串中的括號是否合法,所謂合法,就是括號成對出現

sdf(sd(qwe(qwe)wwe)req)ewq   合法
(sd(qwewqe)asdw(swe))        合法
()()sd()(sd()sw))(           不合法

 

思路:

用for循環遍歷字符串中的每一個字符,對每個字符做如下操作:

  • 遇到左括號,就把左括號壓入棧中
  • 遇到右括號,判斷棧是否為空,為空說明沒有左括號與之對應,字符串不合法;如果棧不為空,則把棧頂元素移除

當for循環結束之后,如果棧是空的,就說明所有的左右括號都抵消掉了,則字符串合法;如果棧中還有元素,則說明缺少右括號,字符串不合法。

// 判斷字符串里的括號是否合法
function isLeagle(string){
  var stack = new Stack()
  for(var i=0; i<string.length; i++){
    var item = string[i]
    // 遇到左括號入棧
    if(item === '('){
      stack.push(item)
    }else if(item === ')'){
      // 遇到右括號,判斷棧是否為空
      if(stack.isEmpty()){
        return false
      }else{
        stack.pop() // 彈出左括號
      }
    }
  }
  // 如果棧為空,說明字符串合法
  return stack.isEmpty()
}

console.log(isLeagle('sdf(sd(qwe(qwe)wwe)req)ewq')) // true
console.log(isLeagle('(sd(qwewqe)asdw(swe))')) // true
console.log(isLeagle('()()sd()(sd()sw))(')) // false

 

練習2:逆波蘭表達式

逆波蘭表達式,也叫后綴表達式,它將復雜表達式轉換為可以依靠簡單的操作得到結果的表達式,例如: (a+b)*(c+d) 轉換為 ab+cd+*

從中綴表達式轉換成后綴表達式的邏輯:

(1)如果遇到 數字 ,就壓入棧

(2)如果遇到 運算符,就彈出棧頂的兩個數字,對這兩個數字進行當前運算符的運算,然后將運算結果壓入棧中

for循環結束之后,棧里只有一個元素,這個元素就是整個表達式的計算結果

示例:

["4", "13", "5", "/", "+"]   等價於 (4 + (13 / 5)) = 6
["10", "6", "9", "3", "+", "-11",  "*", "/", "*", "17", "+", "5", "+"]   
等價於 ((10 * (6 / ((9 + 3) * -11 ))) + 17) +5

請編寫函數 calc_exp(exp) 實現逆波蘭表達式計算,exp的類型是數組

// 計算后綴表達式
function calc_exp(exp){
  var stack = new Stack()
  for(var i=0; i<exp.length; i++){
    var item = exp[i]
    if(["+","-","*","/"].indexOf(item) >= 0){
      var value1 = stack.pop()
      var value2 = stack.pop()
      var exp_str = value2 + item + value1
      // 計算並取整
      var res = parseInt(eval(exp_str))
      // 計算結果壓入棧頂
      stack.push(res.toString())
    }else{
      stack.push(item)
    }
  }
  return stack.pop()
}

console.log(calc_exp(["4", "13", "5", "/", "+"])) // 6

 

練習3:實現一個有min方法的棧

實現一個棧,除了常見的push,pop方法以外,提供一個min方法,返回棧里最小的元素,且時間復雜度為 O(1)

function MinStack(){
  var data_stack = new Stack() // 存儲數據
  var min_stack = new Stack() // 存儲最小值

  this.push = function(item){
    data_stack.push(item)

    // min_stack為空,或者棧頂元素大於item
    if(min_stack.isEmpty() || item < min_stack.top()){
      min_stack.push(item)
    }else{
      min_stack.push(min_stack.top())
    }
  }

  this.pop = function(){
    data_stack.pop()
    min_stack.pop()
  }

  this.min = function(){
    return min_stack.top()
  }
}

var minStack = new MinStack()
minStack.push(3)
minStack.push(5)
minStack.push(8)
minStack.push(1)
console.log(minStack.min()) // 1

 

練習4:使用棧,完成中序表達式轉后續表達式

輸入:["12", "+", "3"]
輸出:["12", "3", "+"]

(1 + (4 + 5 + 3) -3) + (9 + 8) 
輸入:["(", "1", "+", "(", "4", "+", "5", "+", "3", ")", "-", "3", ")", "+", "(", "9", "+", "8", ")"]
輸出:["1", "4", "5", "+", "3", "+", "+", "3", "-", "9", "8", "+", "+"]

(1 + (4 + 5 + 3) / 4 - 3) + (6 + 8) * 3
輸入:["(", "1", "+", "(", "4", "+", "5", "+", "3", ")", "/", "4", "-", "3", ")", "+", "(", "6", "+", "8", ")", "*", "3"]
輸出:["1", "4", "5", "+", "3", "+", "4", "/", "+", "3", "-", "6", "8", "+", "3", "*", "+"]

 

1、如果是數字,直接放入到postfix_list中

2、遇到左括號入棧

3、遇到右括號,把棧頂元素彈出,直到遇到左括號,最后彈出左括號

4、遇到運算符,把棧頂的運算符彈出,直到棧頂的運算符優先級小於當前運算符,把彈出的運算符加入到 postfix_list,當前的運算符入棧

5、for循環結束后,棧里可能還有元素,都彈出放入到 postfix_list 中

var priority_map = {
  "+": 1,
  "-": 1,
  "*": 2,
  "/": 2
}

function infix_exp_2_postfix_epx(exp){
  var stack = new Stack()
  var postfix_list = []
  for(var i=0; i<exp.length; i++){
    var item = exp[i]
    // 如果是數字,直接放入到postfix_list中
    if(!isNaN(item)){
      postfix_list.push(item)
    }else if(item === "("){
      // 遇到左括號入棧
      stack.push(item)
    }else if(item === ")"){
      // 遇到右括號,把棧頂元素彈出直到遇到左括號
      while(stack.top() !== '('){
        postfix_list.push(stack.pop())
      }
      stack.pop() // 左括號出棧
    }else{
      // 遇到運算符,把棧頂的運算符彈出,直到棧頂的運算符優先級小於當前運算符
      while(!stack.isEmpty() && ["+","-","*","/"].indexOf(stack.top()) >= 0 && priority_map[stack.top()] >= priority_map[item]){
        // 把彈出的運算符加入到postfix_list
        postfix_list.push(stack.pop())
      }
      // 當前的運算符入棧
      stack.push(item)
    }
  }

  // for循環結束后,棧里可能還有元素,都彈出放入到 postfix_list中
  while(!stack.isEmpty()){
    postfix_list.push(stack.pop())
  }
  return postfix_list
}

console.log(infix_exp_2_postfix_epx(["12", "+", "3"]))
console.log(infix_exp_2_postfix_epx(["(", "1", "+", "(", "4", "+", "5", "+", "3", ")", "-", "3", ")", "+", "(", "9", "+", "8", ")"]))
console.log(infix_exp_2_postfix_epx(["(", "1", "+", "(", "4", "+", "5", "+", "3", ")", "/", "4", "-", "3", ")", "+", "(", "6", "+", "8", ")", "*", "3"]))

 

隊列

隊列是一種特殊的線性表,它只允許你在隊列的頭部刪除元素,在隊列的末尾添加新元素(先進先出)

定義一個簡單的 Queue 類

function Queue(){
  var items = [] // 存儲數據
}

 

隊列的方法如下:

  • enqueue:從隊列尾部添加一個元素
  • dequeue:從隊列頭部刪除一個元素
  • head:返回頭部的元素(不是刪除)
  • size:返回隊列的大小
  • clear:清空隊列
  • isEmpty:判斷隊列是否為空
  • tall:返回隊列尾節點
function Queue(){
  var items = [] // 存儲數據

  // 向隊列尾部添加一個元素
  this.enqueue = function(item){
    items.push(item)
  }

  // 移除隊列頭部的元素
  this.dequeue = function(){
    return items.shift()
  }

  // 返回隊列頭部的元素
  this.head = function(){
    return items[0]
  }

  // 返回隊列尾部的元素
  this.tail = function(){
    return items[items.length - 1]
  }

  // 返回隊列大小
  this.size = function(){
    return items.length
  }

  // clear
  this.clear = function(){
    items = []
  }

  // 判斷隊列是否為空
  this.isEmpty = function(){
    return items.length === 0
  }
}

 

練習1:約瑟夫環(普通模式)

有一個數組a[100] 存放 0 - 99 ;要求每隔兩個數刪掉一個數,到末尾時循環至開頭繼續進行,求最后一個被刪掉的數。

思路分析:

前10個數是 0 1 2 3 4 5 6 7 8 9 ,所謂每隔兩個數刪掉一個數,其實就是把 2 5 8 刪除掉

算法步驟:

1、從隊列頭部刪除一個元素, index + 1

2、如果 index % 3 === 0 ,就說明這個元素是需要刪除的元素;如果不等於0,就是不需要被刪除的元素,則把它添加到隊列的尾部

// 准備好數據
var arr_list = []
for(var i=0; i<100; i++){
  arr_list.push(i)
}

function del_ring(arr_list){
  // 把數組里的元素都放入到隊列中
  var queue = new Queue()
  for(var i=0; i<arr_list.length; i++){
    queue.enqueue(arr_list[i])
  }
  var index = 0
  while(queue.size() !== 1){
    // 彈出一個元素,判斷是否需要刪除
    var item = queue.dequeue()
    index += 1
    // 每隔兩個就要刪除掉一個,那么不是被刪除的元素就放回到隊列尾部
    if(index % 3 !== 0){
      queue.enqueue(item)
    }
  }

  return queue.head()
}

console.log(del_ring(arr_list))

 

練習2:斐波那契數列(普通模式

使用隊列計算斐波那契數列的第n項

思路分析:

斐波那契數列的前兩項是 1  1 ,此后的每一項都是該項前面兩項之和,即 f(n) = f(n-1) + f(n-2)

先將兩個 1 添加到隊列中,之后使用 while 循環,用index計數,循環終止的條件是 index < n - 2

  • 使用 dequeue 方法從隊列頭部刪除一個元素,該元素為 del_item
  • 使用 head 方法獲得隊列頭部的元素,該元素為 head_item
  • del_item + head_item = next_item,將 next_item 放入隊列,注意,只能從尾部添加元素
  • index + 1

當循環結束時,隊列里面有兩個元素,先用dequeue刪除頭部元素,剩下的那個元素就是我們想要的答案

function fibonacii(n){
  if(n <= 2){
    return 1
  }
  var queue = new Queue()
  var index = 0
  // 先放入斐波那契序列的前兩個數值
  queue.enqueue(1)
  queue.enqueue(1)
  while(index < n-2){ // 循環次數:因為要求的是第n項,但是前面已經放入兩個 1 了
    // 出隊列一個元素
    var del_item = queue.dequeue()
    // 取隊列頭部元素
    var head_item = queue.head()
    var next_item = del_item + head_item
    // 將計算結果放入隊列
    queue.enqueue(next_item)
    index++
  }
  queue.dequeue()
  return queue.head()
}

 

小結:

在socket中,當大量客戶端向服務器發起連接,而服務器忙不過來的時候,就會把這些請求放入到隊列中,先來的先處理,后來的后處理,隊列滿時,新來的請求直接拋棄掉

 

練習3:用隊列實現棧(困難模式)

用兩個隊列實現一個棧

思路:

隊列是先進先出,而棧是先進后出,兩者對數據的管理模式剛好是相反的,但是卻可以用兩個隊列實現一個棧

兩個隊列分別命名為 queue_1,queue_2,實現的思路如下:

  • push:實現push方法時,如果兩個隊列都為空,那么默認向 queue_1 里添加數據,如果有一個不為空,則向這個不為空的隊列里添加數據
  • top:兩個隊列,或者都為空,或者有一個不為空,只需要返回不為空的隊列的尾部元素即可
  • pop:pop方法要刪除的是棧頂,但這個棧頂元素其實是隊列的尾部元素。每一次做pop操作時,將不為空的隊列里的元素依次刪除並放入到另一個隊列中,直到遇到隊列中只剩下一個元素,刪除這個元素,其余的元素都跑到之前為空的隊列中了

在具體的實現中,定義兩個額外的變量,data_queue 和 empty_queue,data_queue 始終指向那個不為空的隊列,empty_queue 始終指向那個為空的隊列

function QueueStack(){
  var queue_1 = new Queue()
  var queue_2 = new Queue()
  var data_queue = null // 放數據的隊列
  var empty_queue = null // 空隊列,備份使用

  // 確認哪個隊列放數據,哪個隊列做備份空隊列
  var init_queue = function(){
    // 都為空,默認返回 queue_1
    if(queue_1.isEmpty() && queue_2.isEmpty()){
      data_queue = queue_1
      empty_queue = queue_2
    }else if(queue_1.isEmpty()){
      data_queue = queue_2
      empty_queue = queue_1
    }else{
      data_queue = queue_1
      empty_queue = queue_2
    }
  }

  this.push = function(item){
    init_queue()
    data_queue.enqueue(item)
  }

  this.top = function(){
    init_queue()
    return data_queue.tail()
  }

  /**
   * pop方法要彈出棧頂元素,這個棧頂元素,其實就是queue的隊尾元素
   * 但是隊尾元素是不能刪除的,我們可以把data_queue里的元素(除了隊尾元素)都移入到empty_queue中
   * 最后移除data_queue的隊尾元素並返回
   * data_queue 和 empty_queue 交換了身份
   */
  this.pop = function(){
    init_queue()
    while(data_queue.size() > 1){
      empty_queue.enqueue(data_queue.dequeue())
    }
    return data_queue.dequeue()
  }
}

 

鏈表

鏈表是物理存儲單元上非連續的、非順序的存儲結構,由一系列節點組成,這里所提到的鏈表,均指單鏈表。

 

 節點

節點包含兩部分,一部分是存儲數據的數據域,一部分是存儲指向下一個節點的指針域。

var Node = function(data){
  this.data = data
  this.next = null
}

var node1 = new Node(1)
var node2 = new Node(2)
var node3 = new Node(5)

node1.next = node2
node2.next = node3

console.log(node1.data) // 1
console.log(node1.next.data) // 2
console.log(node1.next.next.data) // 5

 

首尾節點

鏈表中的第一個節點是首節點,最后一個節點是尾結點

 

有頭鏈表和無頭鏈表

無頭鏈表是指第一個節點既有數據域,又有指針域,第一個節點即是首節點又是頭節點

有頭鏈表是指第一個節點只有指針域,而沒有數據域

 

定義鏈表類

function LinkList(){
  // 定義節點
  var Node = function(data){
    this.data = data
    this.next = null
  }

  var length = 0 // 長度
  var head = null // 頭節點
  var tail = null // 尾結點
}

 

鏈表的方法:

  • append:添加一個新的元素
  • insert:在指定位置插入一個元素
  • remove:刪除指定位置的節點
  • remove_head:刪除首節點
  • remove_tail:刪除尾結點
  • indexOf:返回指定元素的索引
  • get:返回指定索引位置的元素
  • head:返回首節點
  • tail:返回尾結點
  • length:返回鏈表長度
  • isEmpty:判斷鏈表是否為空
  • clear:清空鏈表
  • print:打印整個鏈表

append方法:

// 在尾部添加一個節點
this.append = function(data){
  // 創建新節點
  var new_node = new Node(data)
  if(head === null){
    head = new_node
    tail = new_node
  }else{
    tail.next = new_node
    tail = new_node
  }
  length++
  return true
}

 

print方法:

this.print = function(){
  var curr_node = head
  while(curr_node){
    console.log(curr_node.data)
    curr_node = curr_node.next
  }
}

 

insert方法:

append只能在鏈表的末尾添加元素,而insert可以在指定位置插入一個元素,新增數據的方式更加靈活,insert方法需要傳入參數 index,指明要插入的索引位置。該方法的關鍵是找到索引為 index-1 的節點,只有找到這個節點,才能將新的節點插入到鏈表中。和索引相關的方法,先要考慮索引的范圍是否合法,然后考慮索引的邊界情況。

  • 如果 index === length ,那么可以直接調用 append方法
  • 如果 index > length 或者 index<0,索引錯誤,返回null
  • 如果index===0,創建新節點 new_node,這個新的節點索引是0,因此是鏈表的首節點,讓 new_node.next = head , head = new_node
  • 如果index>0 且index<length,同樣創建新節點 new_node,變量 curr_node 指向 head,insert_index = 1,表示 new_node 應該插入的位置,使用一個 while循環,循環條件是 insert_index < index,在循環體內,insert_index 加一, curr_node = curr_node.next,這樣當循環結束的時候,curr_node 就是new_node的上一個節點,讓 new_node 指向curr_node的下一個節點,而curr_node指向new_node
this.insert = function(index, data){
  if(index < 0 || index > length){
    return false
  }else if(index === length){
    return this.append(data)
  }else{
    var new_node = new Node(data)
    // new_node 成為新的頭結點
    if(index === 0){
      new_node.next = head
      head = new_node
    }else{
      var insert_index = 1
      var curr_node = head
      while(insert_index < index){
        insert_index++
        curr_node = curr_node.next
      }
      // 獲取 next節點(這時還沒將新節點插入)
      var next_node = curr_node.next
      // 改變當前節點的next,指向新節點(這時斬斷了原鏈表,將前半部分的next指向新節點)
      curr_node.next = new_node 
      // 將新節點的next指向原鏈表的后半部分(將斷開的鏈表重新連上)
      new_node.next = next_node
    }
  }
  length++
  return true
}

 

remove方法

 

this.remove = function(index){
  if(index < 0 || index >= length){
    return null
  }else{
    var del_node = null
    if(index === 0){
      del_node = head
      head = head.next
    }else{
      var del_index = 0
      var pre_node = null // 被刪除節點的前一個節點
      var curr_node = head // curr_node就是那個要被刪除的節點
      while(del_index < index){ // 當要刪除的index和傳入的index相同時跳出循環
        del_index++
        pre_node = curr_node // 當前節點賦值給pre_node
        curr_node = curr_node.next // 當前節點往后挪動一位
      }
      del_node = curr_node
      // 被刪除節點的前一個節點指向被刪除節點的后一個節點
      pre_node.next = curr_node.next

      // 如果被刪除的節點是尾結點
      if(curr_node.next = null){
        tail = pre_node // 更新tail節點
      }
    }
    length--
    del_node.next = null
    return del_node.data
  }
}

 

get方法

// 返回指定位置節點的值
this.get = function(index){
  if(index < 0 || index >= length){
    return null
  }
  var node_index = 0
  var curr_node = head
  while(node_index < index){
    node_index++
    curr_node = curr_node.next
  }
  return curr_node.data
}

 

indexOf方法

// 返回指定元素的索引,如果沒有,返回-1
// 有多個相同元素,返回第一個
this.indexOf = function(data){
  var index = -1
  var curr_node = head
  while(curr_node){
    index++
    if(curr_node.data === data){
      return index
    }else{
      curr_node = curr_node.next
    }
  }
  return -1
}

 

練習1:基於鏈表實現的Stack和Queue

function Stack(){
  var linkList = new LinkList()

  // 從棧頂添加元素
  this.push = function(item){
    linkList.append(item)
  }

  // 彈出棧頂元素
  this.pop = function(){
    return linkList.remove_tail()
  }

  // 返回棧頂元素
  this.top = function(){
    return linkList.tail()
  }

  // 返回棧的大小
  this.size = function(){
    return linkList.length()
  }

  // 判斷是否為空
  this.isEmpty = function(){
    return linkList.isEmpty()
  }

  // 清空棧
  this.clear = function(){
    linkList.clear()
  }
}

 

練習2:翻轉鏈表

使用迭代和遞歸兩種方法翻轉鏈表,下面的代碼已經准備好了上下文環境,請實現函數 reverse_iter 和 reverse_digui

var Node = function(data){
  this.data = data
  this.next = null
}

var node1 = new Node(1)
var node2 = new Node(2)
var node3 = new Node(3)
var node4 = new Node(4)
var node5 = new Node(5)

node1.next = node2
node2.next = node3
node3.next = node4
node4.next = node5

function print(node){
  var curr_node = node
  while(curr_node){
    console.log(curr_node.data)
    curr_node = curr_node.next
  }
}

// 迭代翻轉
function reverse_iter(head){
}
// 遞歸翻轉
function reverse_digui(head){
}

 

思路分析:

在考慮算法時,多數情況下考慮邊界情況會讓問題變得簡單,但邊界情況往往不具備普適性,因此,也要嘗試考慮中間的情況,假設鏈表中間的某個點為 curr_node ,它的前一個節點是 pre_node,后一個節點是 next_node,現在把思路聚焦到這個curr_node節點上,只考慮在這一個點上進行翻轉

curr_node.next = pre_node

只需要這簡單的一個步驟就可以完成對 curr_node 節點的翻轉,對於頭結點來說,它沒有上一個節點,讓 pre_node=null ,表示它的上一個節點是空節點。

在遍歷的過程中,每完成一個節點的翻轉,都讓 curr_node = next_node,找到下一個需要翻轉的節點,同時,pre_node 和 next_node 也跟隨 curr_node 一起向后滑動

// 迭代翻轉
function reverse_iter(head){
  if(!head){
    return null
  }
  var pre_node = null // 前一個節點
  var curr_node = head // 當前要翻轉的節點
  while(curr_node){
    var next_node = curr_node.next // 下一個節點
    curr_node.next = pre_node // 對當前節點進行翻轉
    pre_node = curr_node // pre_node向后滑動
    curr_node = next_node // curr_node向后滑動
  }
  // 最后要返回 pre_node,當循環結束時,pre_node指向翻轉前鏈表的最后一個節點
  return pre_node
}
print(reverse_iter(node1))

 

遞歸翻轉鏈表思路分析

遞歸的思想,精髓之處在於甩鍋,你做不到的事情,讓別人去做,等別人做完了,你在別人的基礎上繼續做

甩鍋一共分為四步:

1、明確函數的功能,既然是先讓別人去做,那你得清楚的告訴他做什么。函數 reverse_digui(head) 完成的功能,是從head開始翻轉鏈表,函數返回值是翻轉后的頭結點

 

2、正式甩鍋,進行遞歸調用

var new_head = reverse_digui(head.next)

原本是翻轉以head開頭的鏈表,可是你不會啊,那就先讓別人從head.next開始翻轉鏈表,等他翻轉玩,得到的 new_head 就是翻轉后的頭節點

 

3、根據別人的結果,計算自己的結果

在第二步中,已經完成了從 head.next 開始翻轉鏈表,現在,只需要把 head 鏈接到新鏈表上就可以了,新鏈表的尾結點是 head.next,執行 head.next.next = head,這樣,head 就完成了新鏈表的尾結點

// 遞歸翻轉
function reverse_digui(head){
  // 如果head為null
  if(!head){
    return null
  }
  // 從下一個節點開始進行翻轉
  var new_head = reverse_digui(head.next)
  head.next.next = head // 把當前節點連接到新鏈表上
  head.next = null
  return new_head
}

 

4、找到遞歸的終止條件

遞歸必須有終止條件,否則就會進入死循環,函數最終要返回新鏈表的頭,而新鏈表的頭正是舊鏈表的尾,所以,遇到尾結點,直接返回尾結點,這就是遞歸終止的條件

// 遞歸翻轉
function reverse_digui(head){
  // 如果head為null
  if(!head){
    return null
  }
  if(head.next === null){
    return head
  }
  // 從下一個節點開始進行翻轉
  var new_head = reverse_digui(head.next)
  head.next.next = head // 把當前節點連接到新鏈表上
  head.next = null
  return new_head
}
print(reverse_digui(node1))

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM